Skip to content
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

Merged
merged 17 commits into from
Oct 6, 2023
Merged

Conversation

kupermind
Copy link
Collaborator

  • First implementation of the service staking.

contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved

// Transfer accumulated rewards to the service owner
if (amount > 0) {
_withdraw(msg.sender, amount);
Copy link
Contributor

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!

Copy link
Collaborator Author

@kupermind kupermind Sep 25, 2023

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.

Copy link
Collaborator

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"

Copy link
Contributor

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).
Copy link
Contributor

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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// @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);
Copy link
Contributor

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!

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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);
Copy link
Contributor

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

Copy link
Collaborator

@77ph 77ph Sep 26, 2023

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 

Copy link
Collaborator

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.

Copy link
Contributor

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;
Copy link
Contributor

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

contracts/staking/ServiceStakingBase.sol Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Show resolved Hide resolved
serviceCheckpoint = curInfo.tsStart;
}
// Calculate the nonce ratio in 1e18 value
uint256 ratio = ((block.timestamp - serviceCheckpoint) * 1e18) / (curNonce - curInfo.nonce);
Copy link
Collaborator

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;
Copy link
Collaborator

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

Copy link
Collaborator Author

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
Copy link
Collaborator

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);
Copy link
Collaborator

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.

contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
}
// Calculate the liveness ratio in 1e18 value
uint256 ratio;
// If the checkpoint was called in the exactly same block, the ratio is zero
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Contributor

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor

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.

contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
setServiceIds[idx] = setServiceIds[setServiceIds.length - 1];
setServiceIds.pop();

emit ServiceUnstaked(serviceId, msg.sender, amount, tsStart);
Copy link
Collaborator

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?

contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingToken.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStakingBase.sol Outdated Show resolved Hide resolved
contracts/staking/ServiceStaking.sol Outdated Show resolved Hide resolved
Copy link
Contributor

@DavidMinarsch DavidMinarsch left a 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:

  1. we can say checkpoints can only happen every x time period. If people unstake before they get by default nothing for the rolling period.
  2. we can somehow smoothen the liveness calculation across checkpoints. @mariapiamo might have ideas there?
  3. other ideas...

revert MaxNumServicesReached(maxNumServices);
}

// Check the service conditions for staking
Copy link
Contributor

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.

Copy link
Collaborator Author

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
Copy link
Contributor

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
Copy link
Contributor

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call it ServiceStakingNativeToken?

Comment on lines +62 to +67
if (_stakingToken == address(0) || _serviceRegistryTokenUtility == address(0)) {
revert ZeroAddress();
}

stakingToken = _stakingToken;
serviceRegistryTokenUtility = _serviceRegistryTokenUtility;
Copy link
Contributor

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"

Copy link
Collaborator Author

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.

Comment on lines +77 to +80
// The staking token must match the contract token
if (stakingToken != token) {
revert WrongStakingToken(stakingToken, token);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in constructor

@kupermind
Copy link
Collaborator Author

kupermind commented Sep 29, 2023

TPS average = (block.timestamp - time.start) / (nonce.current - nonce.start)

@kupermind kupermind merged commit db5b1fc into main Oct 6, 2023
2 checks passed
@kupermind kupermind deleted the service_staking branch October 6, 2023 13:31
77ph pushed a commit that referenced this pull request Oct 12, 2023
chore: adding contracts audit scripts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants