Lack of unclaimed rewards recovery mechanism leads to permanent token lockup
Rewards inserted into the claim queue can sit indefinitely if the user never calls claim(). There is no expiration window and no admin path to recover or redistribute the locked balance.
Description
The GenesisLicenseStaking contract lacks a mechanism to handle unclaimed rewards, which can lead to permanent token lockup and protocol inefficiency. When users stake tokens and generate rewards, these rewards are added to a claim queue after the unbonding period.
However, if users never execute the claim() function, these rewards remain in limbo indefinitely.
The issue stems from these key factors:
- Once rewards enter the claim queue through
_insertIntoClaimQueue(), they can only be claimed by the designated user:
function _claim(uint256[] memory indexes_) internal returns (uint256) {// ...require($._userClaimQueueIds[user].contains(index), "Invalid Claim request");// ...}
- There is no expiration mechanism for unclaimed rewards:
// The claim is only checked against the unbonding period, not an expiration daterequire(claimReq.endBlock <= block.number, "Claim request not expired");
- The contract permanently stores claim information even after fulfillment:
// Claims are marked as fulfilled but never deleted$._claimQueueIds.remove(index_);$._fulfilledClaimIds.add(index_);
This design leads to several issues:
- MAT tokens allocated for rewards but never claimed are effectively removed from circulation.
- Contract storage bloat due to accumulation of unclaimed and fulfilled claims.
- Accounting discrepancies between distributed and claimed rewards.
- No administrative mechanism to recover abandoned rewards.
Impact
Tokens reserved for rewards but never claimed are permanently locked, reducing the protocol's effective treasury. Storage bloat grows unboundedly as more claims accumulate.
Recommendation
- Implement a maximum claim period:
uint256 public constant MAX_CLAIM_PERIOD = BLOCKS_PER_DAY * 365; // 1 year expirationfunction _claim(uint256[] memory indexes_) internal returns (uint256) {// ...ClaimReq memory claimReq = $._claimRequests[index];require(claimReq.endBlock <= block.number, "Claim request not expired");// Add expiration checkrequire(block.number <= claimReq.endBlock + MAX_CLAIM_PERIOD, "Claim expired");// ...}
- Add an administrative recovery function for expired claims:
function recoverExpiredClaims(uint256[] calldata indexes_) external onlyOwner {for (uint256 i = 0; i < indexes_.length; ++i) {uint256 index = indexes_[i];require($._claimQueueIds.contains(index), "Invalid claim request");ClaimReq memory claimReq = $._claimRequests[index];require(block.number > claimReq.endBlock + MAX_CLAIM_PERIOD, "Claim not expired");// Remove from claim queue$._claimQueueIds.remove(index);// Optional: transfer to community fund or redistribute// IERC20($._mat).safeTransfer(communityFund, claimReq.amount);emit ClaimExpired(index, claimReq.tokenId, claimReq.amount);}}

