Open howlbot-integration[bot] opened 3 months ago
Good catch. However, we are going to modify in a different place, removing lines 826-829 and leaving line 827, checking that evictServiceIds.length > 0
, otherwise we can reuse serviceIds
array.
kupermind (sponsor) confirmed
0xA5DF marked the issue as selected for report
0xA5DF marked the issue as satisfactory
Fixed
Lines of code
https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingBase.sol#L823 https://github.com/code-423n4/2024-05-olas/blob/3ce502ec8b475885b90668e617f3983cea3ae29f/registries/contracts/staking/StakingBase.sol#L840
Vulnerability details
Impact
When unstake function is called for a specific service id checkpoint function is called and if the checkpoint is successful then the serviceIds array retrieved from the checkpoint function might be outdated/stale because of eviction of some service ids. Due to the use of outdated serviceIds array unstake function reverts.
Proof of Concept
Following is the unstake function
In order to show the error lets select the serviceId to be removed is the last in setServiceIds array. Lets assume that the current setServiceIds (global array) is of length 10. So the service id need to be unstake is setServiceIds[9].The owner of setServiceIds[9] calls the unstake function.
As can be seen from the following lines checkpoint function is called in order to retrieve serviceIds and evictServiceIds(i.e the service ids that have been evicted)
Vulnerability lies in the serviceIds retrieved so lets look at how it is retrieved. Now lets look at the checkpoint function
From the following lines we can see that serviceIds is retrieved from _calculateStakingRewards
_calculateStakingRewards function is as follows
Now if the following check passes i.e if enough time has passed for new epoch to occur if (block.timestamp - tsCheckpointLast >= livenessPeriod && lastAvailableRewards > 0) then as initially as we have assumed that the length of setServiceIds.length = 10 therefore serviceIds length is equal to 10 as can be seen from the following line
As we can see from the above function serviceIds array is just a copy of global setServiceIds array
Note if the liveness ratio doesn't passes then the serviceInactivity of ith service id is increased as can be seen from the above function
Now lets go back to the checkpoint function till now serviceIds is just the copy of global setServiceIds array so till now setServiceIds and serviceIds contain same elements and thus are exact copies.
lets focus on the following lines of code
Now from above we can clearly see that if for some service ids if they have exceeded the max inactivity period then that service id is added to the evictServiceIds array and accordingly numServices which are needed to be evicted are increased.
Now suppose some ids are needed to be evicted i.e numServices >0 thus _evict function is called as follows
lets see the _evict function
Key thing note in the evict function is that it modifies the setServiceIds global array essentially it shrinks the array as it removes the evicted services ids as can be seen from the following line of code
So now serviceIds array and the global setServiceIds array are not same. Neither the index of service ids is same nor the size of both the arrays because there is removal of atleast 1 service id because _evict function was called in the checkpoint function.
Now the following variables are returned by the checkpoint function
Note that the it returns the serviceIds array which is not updated to the global setServiceIds array and thus contain more elements than setServiceIds array.
Now lets go back to the unstake function
Following loop is run in order to check if the service id to be removed is already evicted or is still present in the global setServiceIds array.
Now if the service id to be removed is not evicted then it will be present in the global service ids array but as can be seen from the above the old outdated/stale serviceIds array is used to identify the index of the service id to be evicted. Here is where the issue arises because the index of service id can be different in old/outdated serviceIds array and global setServicesIds array because of eviction of some service ids.
For example initially setServiceIds and serviceIds were exact copy of each other and had 10 service ids i.e size of both the arrays was same. Now suppose service id at index 3 was removed. so now setServiceIds has length 9 and serviceIds has length 10. Also the service id which was at earlier at index 9 in setServicesIds global array is now at index 3 because of how eviction is done using pop functionality. Whereas service id which was earlier at index 9 remains at the same index in service ids array as it is outdated.
Now suppose the owner of service id which is at index 9 in the outdated service and at index 3 in the global array after eviction wants to unstake the service id then it would revert because the above loop will return the index 9 so then the following code will be executed
but the above code will revert because of out of bound error because the above code essentially does the following setServiceIds[9] = setServiceIds[9-1] = setServiceIds[8]
Note that there could be more than one ids evicted so then also unstaking of serivce ids at the second last index would revert.
Tools Used
Manual review
Recommended Mitigation Steps
Make the following change in the checkpoint function where the eviction condition is checked
Assessed type
Context