-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: evict inactive services in the checkpoint #141
Changes from 12 commits
07d1599
e0d6e0e
4afe685
2a99ef5
f0376be
7077fbd
c132997
639044c
4c403f0
84d79c5
1a2b105
fba0b1a
29a37d1
eee7f30
89b1d5e
de4b5d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,10 +70,6 @@ error LowerThan(uint256 provided, uint256 expected); | |
/// @param serviceId Service Id. | ||
error WrongServiceConfiguration(uint256 serviceId); | ||
|
||
/// @dev Service is not staked. | ||
/// @param serviceId Service Id. | ||
error ServiceNotStaked(uint256 serviceId); | ||
|
||
/// @dev Service is not unstaked. | ||
/// @param serviceId Service Id. | ||
error ServiceNotUnstaked(uint256 serviceId); | ||
|
@@ -109,6 +105,12 @@ struct ServiceInfo { | |
/// @author Andrey Lebedev - <[email protected]> | ||
/// @author Mariapia Moscatiello - <[email protected]> | ||
abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | ||
enum ServiceStakingState { | ||
Unstaked, | ||
Staked, | ||
Evicted | ||
} | ||
|
||
// Input staking parameters | ||
struct StakingParams { | ||
// Maximum number of staking services | ||
|
@@ -131,11 +133,13 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
bytes32 configHash; | ||
} | ||
|
||
event ServiceStaked(uint256 indexed serviceId, address indexed owner, address indexed multisig, uint256[] nonces); | ||
event Checkpoint(uint256 availableRewards, uint256 numServices); | ||
event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, address indexed multisig, uint256[] nonces, | ||
uint256 reward, uint256 tsStart); | ||
event ServiceEvicted(uint256 indexed serviceId, address indexed owner, address indexed multisig, uint256 inactivity); | ||
event ServiceStaked(uint256 indexed epoch, uint256 indexed serviceId, address indexed multisig, address owner, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is owner not indexed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's max of 3 indexed fields. I've changed that now, not indexing the epoch. |
||
uint256[] nonces); | ||
event Checkpoint(uint256 indexed epoch, uint256 availableRewards, uint256[] serviceIds, uint256[] rewards); | ||
event ServiceUnstaked(uint256 indexed epoch, uint256 indexed serviceId, address indexed multisig, address owner, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above why tsStart? What's the point of emitting this here? |
||
uint256[] nonces, uint256 reward, uint256 tsStart); | ||
event ServicesEvicted(uint256 indexed epoch, uint256[] serviceIds, address[] owners, address[] multisigs, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. order of multisigs and owners need to be reverted to be symmetric with the other events |
||
uint256[] serviceInactivity); | ||
event Deposit(address indexed sender, uint256 amount, uint256 balance, uint256 availableRewards); | ||
event Withdraw(address indexed to, uint256 amount); | ||
|
||
|
@@ -167,6 +171,8 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
// Max allowed inactivity | ||
uint256 public immutable maxAllowedInactivity; | ||
|
||
// Epoch counter | ||
uint256 public epochCounter; | ||
// Token / ETH balance | ||
uint256 public balance; | ||
// Token / ETH available rewards | ||
|
@@ -329,7 +335,7 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
// Transfer the service for staking | ||
IService(serviceRegistry).safeTransferFrom(msg.sender, address(this), serviceId); | ||
|
||
emit ServiceStaked(serviceId, msg.sender, service.multisig, nonces); | ||
emit ServiceStaked(epochCounter, serviceId, service.multisig, msg.sender, nonces); | ||
} | ||
|
||
/// @dev Gets service multisig nonces. | ||
|
@@ -371,6 +377,7 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
/// @param eligibleServiceRewards Corresponding rewards for eligible service Ids. | ||
/// @param serviceIds All the staking service Ids. | ||
/// @param serviceNonces Current service nonces. | ||
/// @param serviceInactivity Service inactivity records. | ||
function _calculateStakingRewards() internal view returns ( | ||
uint256 lastAvailableRewards, | ||
uint256 numServices, | ||
|
@@ -435,20 +442,80 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
} | ||
} | ||
|
||
/// @dev Evicts services due to their extended inactivity. | ||
/// @param evictServiceIds Service Ids to be evicted. | ||
/// @param serviceInactivity Corresponding service inactivity records. | ||
/// @param numEvictServices Number of services to evict. | ||
function _evict( | ||
uint256[] memory evictServiceIds, | ||
uint256[] memory serviceInactivity, | ||
uint256 numEvictServices | ||
) internal | ||
{ | ||
// Get the total number of staked services | ||
// All the passed arrays have the length of the number of staked services | ||
uint256 totalNumServices = evictServiceIds.length; | ||
|
||
// Get arrays of exact sizes | ||
uint256[] memory serviceIds = new uint256[](numEvictServices); | ||
address[] memory owners = new address[](numEvictServices); | ||
address[] memory multisigs = new address[](numEvictServices); | ||
uint256[] memory inactivity = new uint256[](numEvictServices); | ||
uint256[] memory serviceIndexes = new uint256[](numEvictServices); | ||
|
||
// Fill in arrays | ||
uint256 sCounter; | ||
uint256 serviceId; | ||
for (uint256 i = 0; i < totalNumServices; ++i) { | ||
if (evictServiceIds[i] > 0) { | ||
serviceId = evictServiceIds[i]; | ||
serviceIds[sCounter] = serviceId; | ||
|
||
ServiceInfo storage sInfo = mapServiceInfo[serviceId]; | ||
owners[sCounter] = sInfo.owner; | ||
multisigs[sCounter] = sInfo.multisig; | ||
inactivity[sCounter] = serviceInactivity[i]; | ||
serviceIndexes[sCounter] = i; | ||
sCounter++; | ||
} | ||
} | ||
|
||
// Evict services from the global set of staked services | ||
uint256 idx; | ||
for (uint256 i = numEvictServices - 1; i > 0; --i) { | ||
// Decrease the number of services | ||
totalNumServices--; | ||
// Get the evicted service index | ||
idx = serviceIndexes[i]; | ||
// Assign last service Id to the index that points to the evicted service Id | ||
setServiceIds[idx] = setServiceIds[totalNumServices]; | ||
// Pop the last element | ||
setServiceIds.pop(); | ||
} | ||
|
||
// Deal with the very first element | ||
// Get the evicted service index | ||
idx = serviceIndexes[0]; | ||
// Assign last service Id to the index that points to the evicted service Id | ||
setServiceIds[idx] = setServiceIds[totalNumServices - 1]; | ||
// Pop the last element | ||
setServiceIds.pop(); | ||
|
||
emit ServicesEvicted(epochCounter, serviceIds, owners, multisigs, inactivity); | ||
} | ||
|
||
/// @dev Checkpoint to allocate rewards up until a current time. | ||
/// @return All staking service Ids. | ||
/// @return All staking updated nonces. | ||
/// @return Number of reward-eligible staking services during current checkpoint period. | ||
/// @return Eligible service Ids. | ||
/// @return Eligible service rewards. | ||
/// @return success True, if the checkpoint was successful. | ||
/// @return All staking service Ids (including evicted ones during within a current epoch). | ||
/// @return All staking updated nonces (including evicted ones during within a current epoch). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. during within? |
||
/// @return Set of eligible service Ids. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reward-eligible There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we no longer return the nonces? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We return nonces, we don't return the success flag. No need to return that, just can check for the |
||
/// @return Corresponding set of eligible service rewards. | ||
/// @return evictServiceIds Evicted service Ids. | ||
function checkpoint() public returns ( | ||
uint256[] memory, | ||
uint256[][] memory, | ||
uint256, | ||
uint256[] memory, | ||
uint256[] memory, | ||
bool success | ||
uint256[] memory evictServiceIds | ||
) | ||
{ | ||
// Calculate staking rewards | ||
|
@@ -457,9 +524,16 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
uint256[] memory serviceIds, uint256[][] memory serviceNonces, | ||
uint256[] memory serviceInactivity) = _calculateStakingRewards(); | ||
|
||
// Get arrays for eligible service Ids and rewards of exact size | ||
uint256[] memory finalEligibleServiceIds; | ||
uint256[] memory finalEligibleServiceRewards; | ||
evictServiceIds = new uint256[](serviceIds.length); | ||
uint256 curServiceId; | ||
Comment on lines
+534
to
+537
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the event, we need a precise number of eligible services. Also, evict services are allocated to the size of the all services in the set of staked services. |
||
|
||
// If there are eligible services, proceed with staking calculation and update rewards | ||
if (numServices > 0) { | ||
uint256 curServiceId; | ||
finalEligibleServiceIds = new uint256[](numServices); | ||
finalEligibleServiceRewards = new uint256[](numServices); | ||
// If total allocated rewards are not enough, adjust the reward value | ||
if (totalRewards > lastAvailableRewards) { | ||
// Traverse all the eligible services and adjust their rewards proportional to leftovers | ||
|
@@ -472,17 +546,21 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
updatedTotalRewards += updatedReward; | ||
// Add reward to the overall service reward | ||
curServiceId = eligibleServiceIds[i]; | ||
finalEligibleServiceIds[i] = eligibleServiceIds[i]; | ||
finalEligibleServiceRewards[i] = updatedReward; | ||
mapServiceInfo[curServiceId].reward += updatedReward; | ||
} | ||
|
||
// Process the first service in the set | ||
updatedReward = (eligibleServiceRewards[0] * lastAvailableRewards) / totalRewards; | ||
updatedTotalRewards += updatedReward; | ||
curServiceId = eligibleServiceIds[0]; | ||
finalEligibleServiceIds[0] = eligibleServiceIds[0]; | ||
// If the reward adjustment happened to have small leftovers, add it to the first service | ||
if (lastAvailableRewards > updatedTotalRewards) { | ||
updatedReward += lastAvailableRewards - updatedTotalRewards; | ||
} | ||
finalEligibleServiceRewards[0] = updatedReward; | ||
// Add reward to the overall service reward | ||
mapServiceInfo[curServiceId].reward += updatedReward; | ||
// Set available rewards to zero | ||
|
@@ -492,6 +570,8 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
for (uint256 i = 0; i < numServices; ++i) { | ||
// Add reward to the service overall reward | ||
curServiceId = eligibleServiceIds[i]; | ||
finalEligibleServiceIds[i] = eligibleServiceIds[i]; | ||
finalEligibleServiceRewards[i] = eligibleServiceRewards[i]; | ||
mapServiceInfo[curServiceId].reward += eligibleServiceRewards[i]; | ||
} | ||
|
||
|
@@ -505,30 +585,48 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
|
||
// If service Ids are returned, then the checkpoint takes place | ||
if (serviceIds.length > 0) { | ||
// Updated current service nonces | ||
numServices = 0; | ||
// Record service inactivities and updated current service nonces | ||
for (uint256 i = 0; i < serviceIds.length; ++i) { | ||
// Get the current service Id | ||
uint256 curServiceId = serviceIds[i]; | ||
curServiceId = serviceIds[i]; | ||
// Record service nonces | ||
mapServiceInfo[curServiceId].nonces = serviceNonces[i]; | ||
|
||
// Increase service inactivity if it is greater than zero | ||
if (serviceInactivity[i] > 0) { | ||
mapServiceInfo[curServiceId].inactivity += serviceInactivity[i]; | ||
// Get the overall continuous service inactivity | ||
serviceInactivity[i] = mapServiceInfo[curServiceId].inactivity + serviceInactivity[i]; | ||
mapServiceInfo[curServiceId].inactivity = serviceInactivity[i]; | ||
// Check for the maximum allowed inactivity time | ||
if (serviceInactivity[i] > maxAllowedInactivity) { | ||
// Evict a service if it has been inactive for more than a maximum allowed inactivity time | ||
evictServiceIds[i] = curServiceId; | ||
// Increase number of evicted services | ||
numServices++; | ||
} | ||
} else { | ||
// Otherwise, set it back to zero | ||
mapServiceInfo[curServiceId].inactivity = 0; | ||
} | ||
} | ||
|
||
// Evict inactive services | ||
if (numServices > 0) { | ||
_evict(evictServiceIds, serviceInactivity, numServices); | ||
} | ||
|
||
// Record the current timestamp such that next calculations start from this point of time | ||
tsCheckpoint = block.timestamp; | ||
|
||
success = true; | ||
// Increase the epoch counter | ||
uint256 eCounter = epochCounter; | ||
epochCounter = eCounter + 1; | ||
|
||
emit Checkpoint(lastAvailableRewards, numServices); | ||
emit Checkpoint(eCounter, lastAvailableRewards, finalEligibleServiceIds, finalEligibleServiceRewards); | ||
} | ||
|
||
return (serviceIds, serviceNonces, numServices, eligibleServiceIds, eligibleServiceRewards, success); | ||
return (serviceIds, serviceNonces, finalEligibleServiceIds, finalEligibleServiceRewards, evictServiceIds); | ||
} | ||
|
||
/// @dev Unstakes the service. | ||
|
@@ -540,26 +638,36 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
revert OwnerOnly(msg.sender, sInfo.owner); | ||
} | ||
|
||
// Check that the service has staked long enough, or if there are no rewards left | ||
// Get the staking start time | ||
// Note that if the service info exists, the service is staked or evicted, and thus start time is always valid | ||
uint256 tsStart = sInfo.tsStart; | ||
|
||
// Check that the service has staked long enough, or if there are no rewards left | ||
mariapiamo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
uint256 ts = block.timestamp - tsStart; | ||
if (ts <= maxAllowedInactivity && availableRewards > 0) { | ||
revert NotEnoughTimeStaked(serviceId, ts, maxAllowedInactivity); | ||
} | ||
|
||
// Call the checkpoint | ||
(uint256[] memory serviceIds, , , , , bool success) = checkpoint(); | ||
(uint256[] memory serviceIds, , , , uint256[] memory evictServiceIds) = checkpoint(); | ||
|
||
// If the checkpoint was not successful, the serviceIds set is not returned and needs to be allocated | ||
if (!success) { | ||
if (serviceIds.length == 0) { | ||
serviceIds = getServiceIds(); | ||
evictServiceIds = new uint256[](serviceIds.length); | ||
} | ||
|
||
// Get the service index in the set of services | ||
// The index must always exist as the service is currently staked, otherwise it has no record in the map | ||
uint256 idx; | ||
bool inSet; | ||
for (; idx < serviceIds.length; ++idx) { | ||
if (serviceIds[idx] == serviceId) { | ||
// Service is still in a global staking set if it is found in the services set, | ||
// and is not present in the evicted set | ||
if (evictServiceIds[idx] == serviceId) { | ||
break; | ||
} else if (serviceIds[idx] == serviceId) { | ||
inSet = true; | ||
break; | ||
} | ||
} | ||
|
@@ -575,7 +683,7 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
|
||
// Update the set of staked service Ids | ||
// If the index was not found, the service was evicted and is not part of staked services set | ||
if (idx < serviceIds.length) { | ||
if (inSet) { | ||
setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; | ||
setServiceIds.pop(); | ||
} | ||
|
@@ -588,40 +696,7 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
_withdraw(multisig, reward); | ||
} | ||
|
||
emit ServiceUnstaked(serviceId, msg.sender, multisig, nonces, reward, tsStart); | ||
} | ||
|
||
/// @dev Evicts the service due to its extended inactivity. | ||
function evict(uint256 serviceId) external { | ||
ServiceInfo storage sInfo = mapServiceInfo[serviceId]; | ||
// Get the service inactivity time | ||
uint256 inactivity = sInfo.inactivity; | ||
|
||
// Evict the service if it is inactive more than the max number of inactivity periods | ||
if (inactivity > maxAllowedInactivity) { | ||
// Get service Ids to find the service | ||
uint256[] memory serviceIds = getServiceIds(); | ||
|
||
// Get the service index in the set of services | ||
// The index must always exist if the service is currently staked, otherwise it never existed or was evicted | ||
uint256 idx; | ||
for (; idx < serviceIds.length; ++idx) { | ||
if (serviceIds[idx] == serviceId) { | ||
break; | ||
} | ||
} | ||
|
||
// The service was already evicted | ||
if (idx == serviceIds.length) { | ||
revert ServiceNotFound(serviceId); | ||
} | ||
|
||
// Evict the service from the set of staked services | ||
setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; | ||
setServiceIds.pop(); | ||
|
||
emit ServiceEvicted(serviceId, sInfo.owner, sInfo.multisig, inactivity); | ||
} | ||
emit ServiceUnstaked(epochCounter, serviceId, multisig, msg.sender, nonces, reward, tsStart); | ||
} | ||
|
||
/// @dev Calculates service staking reward during the last checkpoint period. | ||
|
@@ -655,19 +730,20 @@ abstract contract ServiceStakingBase is ERC721TokenReceiver, IErrorsRegistries { | |
ServiceInfo memory sInfo = mapServiceInfo[serviceId]; | ||
reward = sInfo.reward; | ||
|
||
// Check if the service is staked | ||
if (sInfo.tsStart == 0) { | ||
revert ServiceNotStaked(serviceId); | ||
} | ||
|
||
// Add pending reward | ||
reward += calculateServiceStakingLastReward(serviceId); | ||
} | ||
|
||
/// @dev Checks if the service is staked. | ||
/// @dev Gets the service staking state. | ||
/// @param serviceId. | ||
/// @return isStaked True, if the service is staked. | ||
function isServiceStaked(uint256 serviceId) external view returns (bool isStaked) { | ||
isStaked = (mapServiceInfo[serviceId].tsStart > 0); | ||
/// @return stakingState Staking state of the service. | ||
function getServiceStakingState(uint256 serviceId) external view returns (ServiceStakingState stakingState) { | ||
ServiceInfo memory sInfo = mapServiceInfo[serviceId]; | ||
if (sInfo.inactivity > maxAllowedInactivity) { | ||
stakingState = ServiceStakingState.Evicted; | ||
} else if (sInfo.tsStart > 0) { | ||
stakingState = ServiceStakingState.Staked; | ||
} | ||
} | ||
|
||
/// @dev Gets the next reward checkpoint timestamp. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Giving a bit more information about service staking statuses