diff --git a/src/UniStaker.sol b/src/UniStaker.sol index 4bfc378..4cd6690 100644 --- a/src/UniStaker.sol +++ b/src/UniStaker.sol @@ -83,6 +83,9 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @notice Thrown if a caller attempts to specify address zero for certain designated addresses. error UniStaker__InvalidAddress(); + /// @notice Thrown when an onBehalf method is called with a deadline that has expired. + error UniStaker__ExpiredDeadline(); + /// @notice Thrown if a caller supplies an invalid signature to a method that requires one. error UniStaker__InvalidSignature(); @@ -100,25 +103,27 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @notice Type hash used when encoding data for `stakeOnBehalf` calls. bytes32 public constant STAKE_TYPEHASH = keccak256( - "Stake(uint256 amount,address delegatee,address beneficiary,address depositor,uint256 nonce)" + "Stake(uint256 amount,address delegatee,address beneficiary,address depositor,uint256 nonce,uint256 deadline)" ); /// @notice Type hash used when encoding data for `stakeMoreOnBehalf` calls. - bytes32 public constant STAKE_MORE_TYPEHASH = - keccak256("StakeMore(uint256 depositId,uint256 amount,address depositor,uint256 nonce)"); + bytes32 public constant STAKE_MORE_TYPEHASH = keccak256( + "StakeMore(uint256 depositId,uint256 amount,address depositor,uint256 nonce,uint256 deadline)" + ); /// @notice Type hash used when encoding data for `alterDelegateeOnBehalf` calls. bytes32 public constant ALTER_DELEGATEE_TYPEHASH = keccak256( - "AlterDelegatee(uint256 depositId,address newDelegatee,address depositor,uint256 nonce)" + "AlterDelegatee(uint256 depositId,address newDelegatee,address depositor,uint256 nonce,uint256 deadline)" ); /// @notice Type hash used when encoding data for `alterBeneficiaryOnBehalf` calls. bytes32 public constant ALTER_BENEFICIARY_TYPEHASH = keccak256( - "AlterBeneficiary(uint256 depositId,address newBeneficiary,address depositor,uint256 nonce)" + "AlterBeneficiary(uint256 depositId,address newBeneficiary,address depositor,uint256 nonce,uint256 deadline)" ); /// @notice Type hash used when encoding data for `withdrawOnBehalf` calls. - bytes32 public constant WITHDRAW_TYPEHASH = - keccak256("Withdraw(uint256 depositId,uint256 amount,address depositor,uint256 nonce)"); + bytes32 public constant WITHDRAW_TYPEHASH = keccak256( + "Withdraw(uint256 depositId,uint256 amount,address depositor,uint256 nonce,uint256 deadline)" + ); /// @notice Type hash used when encoding data for `claimRewardOnBehalf` calls. bytes32 public constant CLAIM_REWARD_TYPEHASH = - keccak256("ClaimReward(address beneficiary,uint256 nonce)"); + keccak256("ClaimReward(address beneficiary,uint256 nonce,uint256 deadline)"); /// @notice ERC20 token in which rewards are denominated and distributed. IERC20 public immutable REWARD_TOKEN; @@ -310,6 +315,7 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @param _delegatee Address to assign the governance voting weight of the staked tokens. /// @param _beneficiary Address that will accrue rewards for this stake. /// @param _depositor Address of the user on whose behalf this stake is being made. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the user authorizing this stake. /// @return _depositId Unique identifier for this deposit. /// @dev Neither the delegatee nor the beneficiary may be the zero address. @@ -318,14 +324,22 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { address _delegatee, address _beneficiary, address _depositor, + uint256 _deadline, bytes memory _signature ) external returns (DepositIdentifier _depositId) { + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _depositor, _hashTypedDataV4( keccak256( abi.encode( - STAKE_TYPEHASH, _amount, _delegatee, _beneficiary, _depositor, _useNonce(_depositor) + STAKE_TYPEHASH, + _amount, + _delegatee, + _beneficiary, + _depositor, + _useNonce(_depositor), + _deadline ) ) ), @@ -379,21 +393,25 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @param _depositId Unique identifier of the deposit to which stake will be added. /// @param _amount Quantity of stake to be added. /// @param _depositor Address of the user on whose behalf this stake is being made. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the user authorizing this stake. function stakeMoreOnBehalf( DepositIdentifier _depositId, uint256 _amount, address _depositor, + uint256 _deadline, bytes memory _signature ) external { Deposit storage deposit = deposits[_depositId]; _revertIfNotDepositOwner(deposit, _depositor); - + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _depositor, _hashTypedDataV4( keccak256( - abi.encode(STAKE_MORE_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor)) + abi.encode( + STAKE_MORE_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor), _deadline + ) ) ), _signature @@ -419,23 +437,30 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @param _depositId Unique identifier of the deposit which will have its delegatee altered. /// @param _newDelegatee Address of the new governance delegate. /// @param _depositor Address of the user on whose behalf this stake is being made. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the user authorizing this stake. /// @dev The new delegatee may not be the zero address. function alterDelegateeOnBehalf( DepositIdentifier _depositId, address _newDelegatee, address _depositor, + uint256 _deadline, bytes memory _signature ) external { Deposit storage deposit = deposits[_depositId]; _revertIfNotDepositOwner(deposit, _depositor); - + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _depositor, _hashTypedDataV4( keccak256( abi.encode( - ALTER_DELEGATEE_TYPEHASH, _depositId, _newDelegatee, _depositor, _useNonce(_depositor) + ALTER_DELEGATEE_TYPEHASH, + _depositId, + _newDelegatee, + _depositor, + _useNonce(_depositor), + _deadline ) ) ), @@ -462,17 +487,19 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @param _depositId Unique identifier of the deposit which will have its beneficiary altered. /// @param _newBeneficiary Address of the new rewards beneficiary. /// @param _depositor Address of the user on whose behalf this stake is being made. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the user authorizing this stake. /// @dev The new beneficiary may not be the zero address. function alterBeneficiaryOnBehalf( DepositIdentifier _depositId, address _newBeneficiary, address _depositor, + uint256 _deadline, bytes memory _signature ) external { Deposit storage deposit = deposits[_depositId]; _revertIfNotDepositOwner(deposit, _depositor); - + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _depositor, _hashTypedDataV4( @@ -482,7 +509,8 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { _depositId, _newBeneficiary, _depositor, - _useNonce(_depositor) + _useNonce(_depositor), + _deadline ) ) ), @@ -508,22 +536,26 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @param _depositId Unique identifier of the deposit from which stake will be withdrawn. /// @param _amount Quantity of staked token to withdraw. /// @param _depositor Address of the user on whose behalf this stake is being made. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the user authorizing this stake. /// @dev Stake is withdrawn to the deposit owner's account. function withdrawOnBehalf( DepositIdentifier _depositId, uint256 _amount, address _depositor, + uint256 _deadline, bytes memory _signature ) external { Deposit storage deposit = deposits[_depositId]; _revertIfNotDepositOwner(deposit, _depositor); - + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _depositor, _hashTypedDataV4( keccak256( - abi.encode(WITHDRAW_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor)) + abi.encode( + WITHDRAW_TYPEHASH, _depositId, _amount, _depositor, _useNonce(_depositor), _deadline + ) ) ), _signature @@ -541,12 +573,18 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { /// @notice Claim earned reward tokens for a beneficiary, using a signature to validate the /// beneficiary's intent. Tokens are sent to the beneficiary. /// @param _beneficiary Address of the beneficiary who will receive the reward. + /// @param _deadline The timestamp at which the signature should expire. /// @param _signature Signature of the beneficiary authorizing this reward claim. - function claimRewardOnBehalf(address _beneficiary, bytes memory _signature) external { + function claimRewardOnBehalf(address _beneficiary, uint256 _deadline, bytes memory _signature) + external + { + _revertIfPastDeadline(_deadline); _revertIfSignatureIsNotValidNow( _beneficiary, _hashTypedDataV4( - keccak256(abi.encode(CLAIM_REWARD_TYPEHASH, _beneficiary, _useNonce(_beneficiary))) + keccak256( + abi.encode(CLAIM_REWARD_TYPEHASH, _beneficiary, _useNonce(_beneficiary), _deadline) + ) ), _signature ); @@ -816,6 +854,10 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces { if (_account == address(0)) revert UniStaker__InvalidAddress(); } + function _revertIfPastDeadline(uint256 _deadline) internal view { + if (block.timestamp > _deadline) revert UniStaker__ExpiredDeadline(); + } + /// @notice Internal helper method which reverts with UniStaker__InvalidSignature if the signature /// is invalid. /// @param _signer Address of the signer. diff --git a/test/UniStaker.t.sol b/test/UniStaker.t.sol index 3f00097..9d13901 100644 --- a/test/UniStaker.t.sol +++ b/test/UniStaker.t.sol @@ -745,7 +745,7 @@ contract PermitAndStake is UniStakerTest { uint256 _currentNonce ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0)); - _deadline = bound(_deadline, block.timestamp + 1, type(uint256).max); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -790,7 +790,7 @@ contract PermitAndStake is UniStakerTest { uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0)); - _deadline = bound(_deadline, block.timestamp + 1, type(uint256).max); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); vm.assume(_notDepositor != _depositor); @@ -829,9 +829,11 @@ contract StakeOnBehalf is UniStakerTest { uint256 _depositAmount, address _delegatee, address _beneficiary, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -851,7 +853,8 @@ contract StakeOnBehalf is UniStakerTest { _delegatee, _beneficiary, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -860,8 +863,9 @@ contract StakeOnBehalf is UniStakerTest { bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); vm.prank(_sender); - UniStaker.DepositIdentifier _depositId = - uniStaker.stakeOnBehalf(_depositAmount, _delegatee, _beneficiary, _depositor, _signature); + UniStaker.DepositIdentifier _depositId = uniStaker.stakeOnBehalf( + _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature + ); UniStaker.Deposit memory _deposit = _fetchDeposit(_depositId); @@ -878,10 +882,12 @@ contract StakeOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -901,7 +907,8 @@ contract StakeOnBehalf is UniStakerTest { _delegatee, _beneficiary, _depositor, - _suppliedNonce + _suppliedNonce, + _deadline ) ); @@ -911,7 +918,55 @@ contract StakeOnBehalf is UniStakerTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.stakeOnBehalf(_depositAmount, _delegatee, _beneficiary, _depositor, _signature); + uniStaker.stakeOnBehalf( + _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature + ); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _depositorPrivateKey, + address _sender, + uint256 _depositAmount, + address _delegatee, + address _beneficiary, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, 0, block.timestamp - 1); + _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); + address _depositor = vm.addr(_depositorPrivateKey); + _depositAmount = _boundMintAmount(_depositAmount); + _mintGovToken(_depositor, _depositAmount); + + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( + _currentNonce + ); + + vm.prank(_depositor); + govToken.approve(address(uniStaker), _depositAmount); + + bytes32 _message = keccak256( + abi.encode( + uniStaker.STAKE_TYPEHASH(), + _depositAmount, + _delegatee, + _beneficiary, + _depositor, + _currentNonce, + _deadline + ) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.stakeOnBehalf( + _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature + ); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -921,9 +976,11 @@ contract StakeOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( @@ -942,7 +999,8 @@ contract StakeOnBehalf is UniStakerTest { _delegatee, _beneficiary, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -951,21 +1009,25 @@ contract StakeOnBehalf is UniStakerTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter // with an attack-like one. - if (_randomSeed % 5 == 0) { + if (_randomSeed % 6 == 0) { _depositAmount = uint256(keccak256(abi.encode(_depositAmount))); - } else if (_randomSeed % 5 == 1) { + } else if (_randomSeed % 6 == 1) { _delegatee = address(uint160(uint256(keccak256(abi.encode(_delegatee))))); - } else if (_randomSeed % 5 == 2) { + } else if (_randomSeed % 6 == 2) { _depositor = address(uint160(uint256(keccak256(abi.encode(_depositor))))); - } else if (_randomSeed % 5 == 3) { + } else if (_randomSeed % 6 == 3) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 6 == 4) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); - if (_randomSeed % 5 == 4) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 6 == 5) _signature = _modifySignature(_signature, _randomSeed); vm.prank(_sender); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); - uniStaker.stakeOnBehalf(_depositAmount, _delegatee, _beneficiary, _depositor, _signature); + uniStaker.stakeOnBehalf( + _depositAmount, _delegatee, _beneficiary, _depositor, _deadline, _signature + ); } } @@ -1181,7 +1243,7 @@ contract PermitAndStakeMore is UniStakerTest { vm.assume(_delegatee != address(0) && _beneficiary != address(0)); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); - _deadline = bound(_deadline, block.timestamp + 1, type(uint256).max); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); UniStaker.DepositIdentifier _depositId; (_initialDepositAmount, _depositId) = @@ -1236,7 +1298,7 @@ contract PermitAndStakeMore is UniStakerTest { _notDepositorPrivateKey = bound(_notDepositorPrivateKey, 1, 100e18); address _notDepositor = vm.addr(_notDepositorPrivateKey); vm.assume(_depositor != _notDepositor); - _deadline = bound(_deadline, block.timestamp + 1, type(uint256).max); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); UniStaker.DepositIdentifier _depositId; (_initialDepositAmount, _depositId) = @@ -1330,9 +1392,11 @@ contract StakeMoreOnBehalf is UniStakerTest { uint256 _stakeMoreAmount, address _delegatee, address _beneficiary, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); @@ -1358,7 +1422,8 @@ contract StakeMoreOnBehalf is UniStakerTest { _depositId, _stakeMoreAmount, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -1367,7 +1432,7 @@ contract StakeMoreOnBehalf is UniStakerTest { bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); vm.prank(_sender); - uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _signature); + uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature); _deposit = _fetchDeposit(_depositId); @@ -1385,10 +1450,12 @@ contract StakeMoreOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _initialDepositAmount = _boundMintAmount(_initialDepositAmount); @@ -1410,7 +1477,12 @@ contract StakeMoreOnBehalf is UniStakerTest { bytes32 _message = keccak256( abi.encode( - uniStaker.STAKE_MORE_TYPEHASH(), _depositId, _stakeMoreAmount, _depositor, _suppliedNonce + uniStaker.STAKE_MORE_TYPEHASH(), + _depositId, + _stakeMoreAmount, + _depositor, + _suppliedNonce, + _deadline ) ); @@ -1420,7 +1492,58 @@ contract StakeMoreOnBehalf is UniStakerTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _signature); + uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _depositorPrivateKey, + address _sender, + uint256 _initialDepositAmount, + uint256 _stakeMoreAmount, + address _delegatee, + address _beneficiary, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, 0, block.timestamp - 1); + _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); + address _depositor = vm.addr(_depositorPrivateKey); + _initialDepositAmount = _boundMintAmount(_initialDepositAmount); + _mintGovToken(_depositor, _initialDepositAmount); + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( + _currentNonce + ); + + UniStaker.DepositIdentifier _depositId; + (_initialDepositAmount, _depositId) = + _boundMintAndStake(_depositor, _initialDepositAmount, _delegatee, _beneficiary); + + _stakeMoreAmount = _boundToRealisticStake(_stakeMoreAmount); + _mintGovToken(_depositor, _stakeMoreAmount); + + vm.startPrank(_depositor); + govToken.approve(address(uniStaker), _stakeMoreAmount); + vm.stopPrank(); + + bytes32 _message = keccak256( + abi.encode( + uniStaker.STAKE_MORE_TYPEHASH(), + _depositId, + _stakeMoreAmount, + _depositor, + _currentNonce, + _deadline + ) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature); } function testFuzz_RevertIf_DepositorIsNotDepositOwner( @@ -1431,12 +1554,14 @@ contract StakeMoreOnBehalf is UniStakerTest { uint256 _stakeMoreAmount, address _delegatee, address _beneficiary, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _depositor != _notDepositor ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _initialDepositAmount = _boundMintAmount(_initialDepositAmount); _mintGovToken(_depositor, _initialDepositAmount); stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( @@ -1453,7 +1578,7 @@ contract StakeMoreOnBehalf is UniStakerTest { ) ); vm.prank(_sender); - uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _notDepositor, ""); + uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _notDepositor, _deadline, ""); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -1464,9 +1589,11 @@ contract StakeMoreOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _initialDepositAmount = _boundMintAmount(_initialDepositAmount); @@ -1492,7 +1619,8 @@ contract StakeMoreOnBehalf is UniStakerTest { _depositId, _stakeMoreAmount, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -1501,17 +1629,19 @@ contract StakeMoreOnBehalf is UniStakerTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter // with an attack-like one. - if (_randomSeed % 3 == 0) { + if (_randomSeed % 4 == 0) { _stakeMoreAmount = uint256(keccak256(abi.encode(_stakeMoreAmount))); - } else if (_randomSeed % 3 == 1) { + } else if (_randomSeed % 4 == 1) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 4 == 2) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); - if (_randomSeed % 3 == 2) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _signature); + uniStaker.stakeMoreOnBehalf(_depositId, _stakeMoreAmount, _depositor, _deadline, _signature); } } @@ -1653,12 +1783,14 @@ contract AlterDelegateeOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, address _newDelegatee, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _newDelegatee != address(0) ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); @@ -1676,7 +1808,8 @@ contract AlterDelegateeOnBehalf is UniStakerTest { _depositId, _newDelegatee, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -1685,7 +1818,7 @@ contract AlterDelegateeOnBehalf is UniStakerTest { bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); vm.prank(_sender); - uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _signature); + uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature); _deposit = _fetchDeposit(_depositId); @@ -1700,13 +1833,15 @@ contract AlterDelegateeOnBehalf is UniStakerTest { address _beneficiary, address _newDelegatee, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _newDelegatee != address(0) ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _amount = _boundMintAmount(_amount); @@ -1720,7 +1855,12 @@ contract AlterDelegateeOnBehalf is UniStakerTest { bytes32 _message = keccak256( abi.encode( - uniStaker.ALTER_DELEGATEE_TYPEHASH(), _depositId, _newDelegatee, _depositor, _suppliedNonce + uniStaker.ALTER_DELEGATEE_TYPEHASH(), + _depositId, + _newDelegatee, + _depositor, + _suppliedNonce, + _deadline ) ); @@ -1730,7 +1870,53 @@ contract AlterDelegateeOnBehalf is UniStakerTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _signature); + uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _depositorPrivateKey, + address _sender, + uint256 _amount, + address _delegatee, + address _beneficiary, + address _newDelegatee, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume( + _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) + && _newDelegatee != address(0) + ); + _deadline = bound(_deadline, 0, block.timestamp - 1); + _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); + address _depositor = vm.addr(_depositorPrivateKey); + _amount = _boundMintAmount(_amount); + _mintGovToken(_depositor, _amount); + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( + _currentNonce + ); + + UniStaker.DepositIdentifier _depositId; + (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary); + + bytes32 _message = keccak256( + abi.encode( + uniStaker.ALTER_DELEGATEE_TYPEHASH(), + _depositId, + _newDelegatee, + _depositor, + _currentNonce, + _deadline + ) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature); } function testFuzz_RevertIf_DepositorIsNotDepositOwner( @@ -1741,12 +1927,14 @@ contract AlterDelegateeOnBehalf is UniStakerTest { address _delegatee, address _newDelegatee, address _beneficiary, + uint256 _deadline, bytes memory _signature ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _depositor != _notDepositor ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _amount = _boundMintAmount(_amount); _mintGovToken(_depositor, _amount); @@ -1759,7 +1947,9 @@ contract AlterDelegateeOnBehalf is UniStakerTest { ) ); vm.prank(_sender); - uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _notDepositor, _signature); + uniStaker.alterDelegateeOnBehalf( + _depositId, _newDelegatee, _notDepositor, _deadline, _signature + ); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -1770,9 +1960,11 @@ contract AlterDelegateeOnBehalf is UniStakerTest { address _beneficiary, address _newDelegatee, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _amount = _boundMintAmount(_amount); @@ -1790,7 +1982,8 @@ contract AlterDelegateeOnBehalf is UniStakerTest { _depositId, _newDelegatee, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -1799,17 +1992,19 @@ contract AlterDelegateeOnBehalf is UniStakerTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter // with an attack-like one. - if (_randomSeed % 3 == 0) { + if (_randomSeed % 4 == 0) { _newDelegatee = address(uint160(uint256(keccak256(abi.encode(_newDelegatee))))); - } else if (_randomSeed % 3 == 1) { + } else if (_randomSeed % 4 == 1) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 4 == 2) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); - if (_randomSeed % 3 == 2) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _signature); + uniStaker.alterDelegateeOnBehalf(_depositId, _newDelegatee, _depositor, _deadline, _signature); } } @@ -1947,12 +2142,14 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, address _newBeneficiary, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _newBeneficiary != address(0) ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); @@ -1970,7 +2167,8 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { _depositId, _newBeneficiary, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -1979,7 +2177,9 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); vm.prank(_sender); - uniStaker.alterBeneficiaryOnBehalf(_depositId, _newBeneficiary, _depositor, _signature); + uniStaker.alterBeneficiaryOnBehalf( + _depositId, _newBeneficiary, _depositor, _deadline, _signature + ); _deposit = _fetchDeposit(_depositId); @@ -1994,13 +2194,15 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { address _beneficiary, address _newBeneficiary, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _newBeneficiary != address(0) ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _amount = _boundMintAmount(_amount); @@ -2018,7 +2220,8 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { _depositId, _newBeneficiary, _depositor, - _suppliedNonce + _suppliedNonce, + _deadline ) ); @@ -2028,7 +2231,57 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.alterBeneficiaryOnBehalf(_depositId, _newBeneficiary, _depositor, _signature); + uniStaker.alterBeneficiaryOnBehalf( + _depositId, _newBeneficiary, _depositor, _deadline, _signature + ); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _depositorPrivateKey, + address _sender, + uint256 _amount, + address _delegatee, + address _beneficiary, + address _newBeneficiary, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume( + _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) + && _newBeneficiary != address(0) + ); + _deadline = bound(_deadline, 0, block.timestamp - 1); + _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); + address _depositor = vm.addr(_depositorPrivateKey); + _amount = _boundMintAmount(_amount); + _mintGovToken(_depositor, _amount); + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( + _currentNonce + ); + + UniStaker.DepositIdentifier _depositId; + (_amount, _depositId) = _boundMintAndStake(_depositor, _amount, _delegatee, _beneficiary); + + bytes32 _message = keccak256( + abi.encode( + uniStaker.ALTER_BENEFICIARY_TYPEHASH(), + _depositId, + _newBeneficiary, + _depositor, + _currentNonce, + _deadline + ) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.alterBeneficiaryOnBehalf( + _depositId, _newBeneficiary, _depositor, _deadline, _signature + ); } function testFuzz_RevertIf_DepositorIsNotDepositOwner( @@ -2039,12 +2292,14 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, address _newBeneficiary, + uint256 _deadline, bytes memory _signature ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _depositor != _notDepositor ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _amount = _boundMintAmount(_amount); _mintGovToken(_depositor, _amount); @@ -2057,7 +2312,9 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { ) ); vm.prank(_sender); - uniStaker.alterBeneficiaryOnBehalf(_depositId, _newBeneficiary, _notDepositor, _signature); + uniStaker.alterBeneficiaryOnBehalf( + _depositId, _newBeneficiary, _notDepositor, _deadline, _signature + ); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -2068,9 +2325,11 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { address _beneficiary, address _newBeneficiary, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _amount = _boundMintAmount(_amount); @@ -2088,7 +2347,8 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { _depositId, _newBeneficiary, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -2097,17 +2357,21 @@ contract AlterBeneficiaryOnBehalf is UniStakerTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter // with an attack-like one. - if (_randomSeed % 3 == 0) { + if (_randomSeed % 4 == 0) { _newBeneficiary = address(uint160(uint256(keccak256(abi.encode(_newBeneficiary))))); - } else if (_randomSeed % 3 == 1) { + } else if (_randomSeed % 4 == 1) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 4 == 2) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); - if (_randomSeed % 3 == 2) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.alterBeneficiaryOnBehalf(_depositId, _newBeneficiary, _depositor, _signature); + uniStaker.alterBeneficiaryOnBehalf( + _depositId, _newBeneficiary, _depositor, _deadline, _signature + ); } } @@ -2410,9 +2674,11 @@ contract WithdrawOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _withdrawAmount, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); @@ -2432,7 +2698,8 @@ contract WithdrawOnBehalf is UniStakerTest { _depositId, _withdrawAmount, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -2441,7 +2708,7 @@ contract WithdrawOnBehalf is UniStakerTest { bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); vm.prank(_sender); - uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _signature); + uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature); _deposit = _fetchDeposit(_depositId); @@ -2456,10 +2723,12 @@ contract WithdrawOnBehalf is UniStakerTest { address _beneficiary, uint256 _withdrawAmount, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -2474,7 +2743,12 @@ contract WithdrawOnBehalf is UniStakerTest { bytes32 _message = keccak256( abi.encode( - uniStaker.WITHDRAW_TYPEHASH(), _depositId, _withdrawAmount, _depositor, _suppliedNonce + uniStaker.WITHDRAW_TYPEHASH(), + _depositId, + _withdrawAmount, + _depositor, + _suppliedNonce, + _deadline ) ); @@ -2484,7 +2758,51 @@ contract WithdrawOnBehalf is UniStakerTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _signature); + uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _depositorPrivateKey, + address _sender, + uint256 _depositAmount, + address _delegatee, + address _beneficiary, + uint256 _withdrawAmount, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, 0, block.timestamp - 1); + _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); + address _depositor = vm.addr(_depositorPrivateKey); + _depositAmount = _boundMintAmount(_depositAmount); + _mintGovToken(_depositor, _depositAmount); + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_depositor).checked_write( + _currentNonce + ); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + + bytes32 _message = keccak256( + abi.encode( + uniStaker.WITHDRAW_TYPEHASH(), + _depositId, + _withdrawAmount, + _depositor, + _currentNonce, + _deadline + ) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature); } function testFuzz_RevertIf_DepositorIsNotDepositOwner( @@ -2495,12 +2813,14 @@ contract WithdrawOnBehalf is UniStakerTest { address _delegatee, address _beneficiary, uint256 _withdrawAmount, + uint256 _deadline, bytes memory _signature ) public { vm.assume( _delegatee != address(0) && _beneficiary != address(0) && _sender != address(0) && _depositor != _notDepositor ); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _amount = _boundMintAmount(_amount); _mintGovToken(_depositor, _amount); @@ -2513,7 +2833,7 @@ contract WithdrawOnBehalf is UniStakerTest { ) ); vm.prank(_sender); - uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _notDepositor, _signature); + uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _notDepositor, _deadline, _signature); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -2524,9 +2844,11 @@ contract WithdrawOnBehalf is UniStakerTest { address _beneficiary, uint256 _withdrawAmount, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _beneficiary != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _depositorPrivateKey = bound(_depositorPrivateKey, 1, 100e18); address _depositor = vm.addr(_depositorPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -2545,7 +2867,8 @@ contract WithdrawOnBehalf is UniStakerTest { _depositId, _withdrawAmount, _depositor, - uniStaker.nonces(_depositor) + uniStaker.nonces(_depositor), + _deadline ) ); @@ -2554,17 +2877,19 @@ contract WithdrawOnBehalf is UniStakerTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit parameter // with an attack-like one. - if (_randomSeed % 3 == 0) { + if (_randomSeed % 4 == 0) { _withdrawAmount = uint256(keccak256(abi.encode(_withdrawAmount))); - } else if (_randomSeed % 3 == 1) { + } else if (_randomSeed % 4 == 1) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 4 == 2) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_depositorPrivateKey, _messageHash); - if (_randomSeed % 3 == 2) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _signature); + uniStaker.withdrawOnBehalf(_depositId, _withdrawAmount, _depositor, _deadline, _signature); } } @@ -4641,7 +4966,8 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { uint256 _rewardAmount, address _delegatee, address _depositor, - uint256 _currentNonce + uint256 _currentNonce, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0)); _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18); @@ -4658,6 +4984,7 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); uint256 _earned = uniStaker.unclaimedReward(_beneficiary); @@ -4666,7 +4993,9 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { ); bytes32 _message = keccak256( - abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary)) + abi.encode( + uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary), _deadline + ) ); bytes32 _messageHash = @@ -4674,7 +5003,7 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash); vm.prank(_sender); - uniStaker.claimRewardOnBehalf(_beneficiary, _signature); + uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); assertEq(rewardToken.balanceOf(_beneficiary), _earned); } @@ -4688,7 +5017,8 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { address _delegatee, address _depositor, uint256 _currentNonce, - uint256 _suppliedNonce + uint256 _suppliedNonce, + uint256 _deadline ) public { vm.assume(_currentNonce != _suppliedNonce); vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0)); @@ -4706,13 +5036,15 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { _mintTransferAndNotifyReward(_rewardAmount); // A portion of the duration passes _jumpAheadByPercentOfRewardDuration(_durationPercent); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write( _currentNonce ); - bytes32 _message = - keccak256(abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, _suppliedNonce)); + bytes32 _message = keccak256( + abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, _suppliedNonce, _deadline) + ); bytes32 _messageHash = keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); @@ -4720,7 +5052,52 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.claimRewardOnBehalf(_beneficiary, _signature); + uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); + } + + function testFuzz_RevertIf_DeadlineExpired( + uint256 _beneficiaryPrivateKey, + address _sender, + uint256 _depositAmount, + uint256 _durationPercent, + uint256 _rewardAmount, + address _delegatee, + address _depositor, + uint256 _currentNonce, + uint256 _deadline + ) public { + vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0)); + _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18); + address _beneficiary = vm.addr(_beneficiaryPrivateKey); + + UniStaker.DepositIdentifier _depositId; + (_depositAmount, _rewardAmount) = _boundToRealisticStakeAndReward(_depositAmount, _rewardAmount); + _durationPercent = bound(_durationPercent, 0, 100); + + // A user deposits staking tokens + (_depositAmount, _depositId) = + _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); + // The contract is notified of a reward + _mintTransferAndNotifyReward(_rewardAmount); + // A portion of the duration passes + _jumpAheadByPercentOfRewardDuration(_durationPercent); + _deadline = bound(_deadline, 0, block.timestamp - 1); + + stdstore.target(address(uniStaker)).sig("nonces(address)").with_key(_beneficiary).checked_write( + _currentNonce + ); + + bytes32 _message = keccak256( + abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, _currentNonce, _deadline) + ); + + bytes32 _messageHash = + keccak256(abi.encodePacked("\x19\x01", EIP712_DOMAIN_SEPARATOR, _message)); + bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash); + + vm.expectRevert(UniStaker.UniStaker__ExpiredDeadline.selector); + vm.prank(_sender); + uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); } function testFuzz_RevertIf_InvalidSignatureIsPassed( @@ -4730,9 +5107,11 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { address _depositor, address _delegatee, uint256 _currentNonce, - uint256 _randomSeed + uint256 _randomSeed, + uint256 _deadline ) public { vm.assume(_delegatee != address(0) && _depositor != address(0) && _sender != address(0)); + _deadline = bound(_deadline, block.timestamp, type(uint256).max); _beneficiaryPrivateKey = bound(_beneficiaryPrivateKey, 1, 100e18); address _beneficiary = vm.addr(_beneficiaryPrivateKey); _depositAmount = _boundMintAmount(_depositAmount); @@ -4746,7 +5125,9 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { _boundMintAndStake(_depositor, _depositAmount, _delegatee, _beneficiary); bytes32 _message = keccak256( - abi.encode(uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary)) + abi.encode( + uniStaker.CLAIM_REWARD_TYPEHASH(), _beneficiary, uniStaker.nonces(_beneficiary), _deadline + ) ); bytes32 _messageHash = @@ -4754,17 +5135,19 @@ contract ClaimRewardOnBehalf is UniStakerRewardsTest { // Here we use `_randomSeed` as an arbitrary source of randomness to replace a legit // parameter with an attack-like one. - if (_randomSeed % 3 == 0) { + if (_randomSeed % 4 == 0) { _beneficiary = address(uint160(uint256(keccak256(abi.encode(_beneficiary))))); - } else if (_randomSeed % 3 == 1) { + } else if (_randomSeed % 4 == 1) { _messageHash = _modifyMessage(_messageHash, uint256(keccak256(abi.encode(_randomSeed)))); + } else if (_randomSeed % 4 == 2) { + _deadline = uint256(keccak256(abi.encode(_deadline))); } bytes memory _signature = _sign(_beneficiaryPrivateKey, _messageHash); - if (_randomSeed % 3 == 2) _signature = _modifySignature(_signature, _randomSeed); + if (_randomSeed % 4 == 3) _signature = _modifySignature(_signature, _randomSeed); vm.expectRevert(UniStaker.UniStaker__InvalidSignature.selector); vm.prank(_sender); - uniStaker.claimRewardOnBehalf(_beneficiary, _signature); + uniStaker.claimRewardOnBehalf(_beneficiary, _deadline, _signature); } }