-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: first implementation of the service staking #116
Conversation
kupermind
commented
Sep 21, 2023
- First implementation of the service staking.
|
||
// Transfer accumulated rewards to the service owner | ||
if (amount > 0) { | ||
_withdraw(msg.sender, amount); |
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.
Needs to be sent to multisig, not service owner!
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.
Just to make sure: the agents of the multisig then have to transfer rewards somewhere later or utilize rewards for the service's needs? Also it means that the service owner is not getting anything directly, although they were staking the service.
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.
I understand correctly that receiving a reward is not a mirror (asymmetrical) operation to "staking"
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.
Yes the service logic is responsible for deciding where rewards go.
uint256[] public setServiceIds; | ||
|
||
/// @dev ServiceStakingBase constructor. | ||
/// @param _apy Staking APY (in single digits). |
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.
apy - not sure that's appropriate terminology
/// @dev ServiceStakingBase constructor. | ||
/// @param _apy Staking APY (in single digits). | ||
/// @param _minStakingDeposit Minimum security deposit for a service to be eligible to stake. | ||
/// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). |
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.
/// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). | |
/// @param _livenessRatio Liveness ratio: number of seconds per nonce (in 18 digits). |
seems more appropriate
serviceCheckpoint = curInfo.tsStart; | ||
} | ||
// Calculate the nonce ratio in 1e18 value | ||
uint256 ratio = ((block.timestamp - serviceCheckpoint) * 1e18) / (curNonce - curInfo.nonce); |
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.
this is the wrong way round. More tx is more liveness!
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.
Also prevent division by zero
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.
I can see an issue here with closeness of checkpoints leading to rounding issues
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.
This can be defined in either direction. If we believe there will be more tx-s than seconds, then this must be reversed. We are still checking on a specific period of time (from last checkpoint till now) and how many tx-s were performed by every staking service within that period.
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.
Agree, incorrect.
It should be tps with dimension: nonce/s (tx/s). In the code we got the inverse value.
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.
yeah, tps is the inverse. Plus denominator cannot be zero
uint256 newAvailableRewards = availableRewards + amount; | ||
|
||
// Update rewards per second | ||
uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); |
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.
I'd prefer if rewards per second was an arg rather than apy
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.
So as I understand what we want:
A given constant RewardsPerSecond (RPS) with dimension [T/s] T - token (including ETH)
RewardsPerSecond : immutable, and accordingly does not depend on the current AvailableRewards
Formula reward = RPS * (t2 - t1) (1*)
A simple example of how this works without boundary conditions ( AvailableRewards >> sum(rewards)):
0. Start staking with AvailableRewards = 1M Tokens, no staking services, RPS = 1T/s
1. Staking 3 services: s1, s2, s3
2. +24h. Unstaking 3 services
s1 = pass tps_ratio => reward1 (yes)
s2 = pass tps_ratio => reward2 (yes)
s3 = fail tps_ratio => 0 (no)
reward1 = RPS * 24*60*60s = 1T/s * 86400s = 86400 T
reward2 = RPS * 24*60*60s = 1T/s * 86400s = 86400 T
AvailableRewards = AvailableRewards - sum(rewards) = 1M - 2*86400 = 827,200
Let's assume we still have AvailableRewards = 100k
0. No staking atm
1. Staking 3 services: s1, s2, s3
2. +24h. Unstaking 3 services
s1 = pass tps_ratio => reward1 (yes)
s2 = pass tps_ratio => reward2 (yes)
s3 = fail tps_ratio => 0 (no)
reward1 = RPS * 24*60*60s = 1T/s * 86400s = 86400 T (incorrect)
reward2 = RPS * 24*60*60s = 1T/s * 86400s = 86400 T (incorrect)
AvailableRewards = AvailableRewards - sum(rewards) = 100k - 2*86400 < 0
If AvailableRewards < sum(rewards) by (1*):
We need to distribute rewards based on the current limit in proportion to the "ideal" reward (1*)
reward1fix = RPS * 24*60*60s /sum(rewards) * AvailableRewards = 1/2 * 100k = 50k T
reward2fix = RPS * 24*60*60s /sum(rewards) * AvailableRewards = 1/2 * 100k = 50k T
AvailableRewards = AvailableRewards - 100k = 0
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.
With your suggestion @DavidMinarsch and the formula of @77ph the contract becomes significantly simpler. However, I have concerns because staking rewards are entirely unrelated to the service security deposit amount. This could potentially lead to a situation where staking rewards greatly exceed the service security deposits.
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.
"I have concerns because staking rewards are entirely unrelated to the service security deposit amount." -> that's up to the contract deployer. They set both after all.
|
||
// Adjust the available rewards value | ||
if (lastAvailableRewards >= reward) { | ||
lastAvailableRewards -= reward; |
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.
use memory var in loop when optimising later
serviceCheckpoint = curInfo.tsStart; | ||
} | ||
// Calculate the nonce ratio in 1e18 value | ||
uint256 ratio = ((block.timestamp - serviceCheckpoint) * 1e18) / (curNonce - curInfo.nonce); |
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.
yeah, tps is the inverse. Plus denominator cannot be zero
|
||
// Process each eligible service Id reward | ||
// Calculate the maximum possible reward per service during the last deposit period | ||
uint256 maxRewardsPerService = (rewardsPerSecond * (block.timestamp - tsCheckpointLast)) / numServices; |
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.
How large can be rewardsPerSecond? Worried when the numSevices is small
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.
This is not applicable anymore
serviceCheckpoint = curInfo.tsStart; | ||
} | ||
|
||
// If the staking was longer than the deposited period, the service's timestamp is adjusted such that |
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.
not sure about the following
uint256 newAvailableRewards = availableRewards + amount; | ||
|
||
// Update rewards per second | ||
uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); |
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.
With your suggestion @DavidMinarsch and the formula of @77ph the contract becomes significantly simpler. However, I have concerns because staking rewards are entirely unrelated to the service security deposit amount. This could potentially lead to a situation where staking rewards greatly exceed the service security deposits.
} | ||
// Calculate the liveness ratio in 1e18 value | ||
uint256 ratio; | ||
// If the checkpoint was called in the exactly same block, the ratio is zero |
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.
what if there is a checkpoint at the beginning of the block, one at the end, and in the middle one service multisig has done some transactions? We just update the nonce info at the end on this checkpoint, but all the transaction in between are not accounted for incentives
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.
I don't see the issue here, as the last nonce will be registered at the moment of the checkpoint, so if more nonces are going to be in that block, they will be picked up the next time.
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.
I guess @mariapiamo's point requires >= rather than >?
mapServiceInfo[curServiceId].reward += updatedReward; | ||
} | ||
|
||
// If the reward adjustment happened to have small leftovers, add it to the last traversed service |
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.
So last service in the list can receive a bit more?
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.
yes
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.
Is reverse loop more expensive? Makes sense to have the first staked service have this privilige.
setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; | ||
setServiceIds.pop(); | ||
|
||
emit ServiceUnstaked(serviceId, msg.sender, amount, tsStart); |
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.
can we emit also service multisig address and nonce?
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.
Nice progress!
My biggest concern is now what I mentioned this morning: with this implementation it can happen that frequent staking/unstaking/checkpoint calls leads to "unjust" reward allocations simply because the liveness check is missed due to non-equally spaced transactions.
There's different ways to solve this:
- we can say checkpoints can only happen every x time period. If people unstake before they get by default nothing for the rolling period.
- we can somehow smoothen the liveness calculation across checkpoints. @mariapiamo might have ideas there?
- other ideas...
revert MaxNumServicesReached(maxNumServices); | ||
} | ||
|
||
// Check the service conditions for staking |
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.
I notice a nice optional feature which I'd like to support from the beginning: a staking contract can optionally specify an exact agentID and hash for the service that it requires to be staked. Then here, we check the agentId and service hash as well if and only if the hash is provided in the staking contract.
This would allow staking contracts to express exactly what service they want at which version.
This also raises the question if we should allow the staking contract to specify the number of agent instances and threshold.
These four things together tie down the exact configuration.
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.
ok that's nice
} | ||
// Calculate the liveness ratio in 1e18 value | ||
uint256 ratio; | ||
// If the checkpoint was called in the exactly same block, the ratio is zero |
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.
I guess @mariapiamo's point requires >= rather than >?
mapServiceInfo[curServiceId].reward += updatedReward; | ||
} | ||
|
||
// If the reward adjustment happened to have small leftovers, add it to the last traversed service |
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.
Is reverse loop more expensive? Makes sense to have the first staked service have this privilige.
/// @author Aleksandr Kuperman - <[email protected]> | ||
/// @author Andrey Lebedev - <[email protected]> | ||
/// @author Mariapia Moscatiello - <[email protected]> | ||
contract ServiceStaking is ServiceStakingBase { |
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.
Maybe call it ServiceStakingNativeToken?
if (_stakingToken == address(0) || _serviceRegistryTokenUtility == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
stakingToken = _stakingToken; | ||
serviceRegistryTokenUtility = _serviceRegistryTokenUtility; |
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.
Should also check that the staking token matches the one used for security deposit? I mean in principle one could break this symmetry. But I think we should then have a different contract. Symmetry is more "expected"
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.
We check that for each service itself. ServiceRegistryTokenUtility is not bound to a specific custom token.
// The staking token must match the contract token | ||
if (stakingToken != token) { | ||
revert WrongStakingToken(stakingToken, token); | ||
} |
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.
Also in constructor
TPS average = (block.timestamp - time.start) / (nonce.current - nonce.start) |
chore: adding contracts audit scripts