From 2487fb6610f111543cef213e1424e18f86c07056 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Thu, 21 Sep 2023 19:21:16 +0100 Subject: [PATCH 01/16] feat: first implementation of the service staking --- contracts/staking/ServiceStaking.sol | 45 +++++ contracts/staking/ServiceStakingBase.sol | 190 ++++++++++++++++++++++ contracts/staking/ServiceStakingToken.sol | 156 ++++++++++++++++++ hardhat.config.js | 2 +- 4 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 contracts/staking/ServiceStaking.sol create mode 100644 contracts/staking/ServiceStakingBase.sol create mode 100644 contracts/staking/ServiceStakingToken.sol diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol new file mode 100644 index 00000000..2a5cbd04 --- /dev/null +++ b/contracts/staking/ServiceStaking.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "./ServiceStakingBase.sol"; + +/// @dev Failure of a transfer. +/// @param token Address of a token. +/// @param from Address `from`. +/// @param to Address `to`. +/// @param value Value. +error TransferFailed(address token, address from, address to, uint256 value); + +/// @title ServiceStakingToken - Smart contract for staking the service by its owner based ETH as a deposit +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +contract ServiceStaking is ServiceStakingBase { + constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) + ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) + {} + + function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { + // Get the service security token and deposit + (uint96 securityDeposit, , , , , , ) = IService(serviceRegistry).mapServices(serviceId); + + // The security deposit must be greater or equal to the minimum defined one + if (securityDeposit < minSecurityDeposit) { + revert(); + } + } + + function _withdraw(address to, uint256 amount) internal override { + (bool result, ) = to.call{value: amount}(""); + if (!result) { + revert TransferFailed(address(0), address(this), to, amount); + } + } + + receive() external payable { + // Distribute current staking rewards + _checkpoint(0); + + // Add to the overall balance + balance += msg.value; + } +} \ No newline at end of file diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol new file mode 100644 index 00000000..28c1a525 --- /dev/null +++ b/contracts/staking/ServiceStakingBase.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +interface IMultisig { + function nonce() external returns (uint256); +} + +interface IService { + function transferFrom(address from, address to, uint256 id) external; + + /// @dev Gets the service instance from the map of services. + /// @param serviceId Service Id. + /// @return securityDeposit Registration activation deposit. + /// @return multisig Service multisig address. + /// @return configHash IPFS hashes pointing to the config metadata. + /// @return threshold Agent instance signers threshold. + /// @return maxNumAgentInstances Total number of agent instances. + /// @return numAgentInstances Actual number of agent instances. + /// @return state Service state. + function mapServices(uint256 serviceId) external view returns ( + uint96 securityDeposit, + address multisig, + bytes32 configHash, + uint32 threshold, + uint32 maxNumAgentInstances, + uint32 numAgentInstances, + uint8 state + ); +} + +struct ServiceInfo { + address multisig; + address owner; + uint256 nonce; + uint256 ts; + uint256 reward; +} + +/// @title ServiceStakingBase - Base abstract smart contract for staking the service by its owner +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +abstract contract ServiceStakingBase { + // APY value + uint256 public immutable apy; + // Minimum deposit value for staking + uint256 public immutable minSecurityDeposit; + // Staking ratio in the format of 1e18 + uint256 public immutable stakingRatio; + // ServiceRegistry contract address + address public immutable serviceRegistry; + + // Token / ETH balance + uint256 public balance; + // Timestamp of the last checkpoint + uint256 public tsLastDeposit; + // Minimum balance going below which would be considered that the balance is zero + uint256 public minBalance; + // Mapping of serviceId => staking service info + mapping (uint256 => ServiceInfo) public mapServiceInfo; + // Set of currently staking serviceIds + uint256[] public setServiceIds; + + constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) { + apy = _apy; + minSecurityDeposit = _minSecurityDeposit; + stakingRatio = _stakingRatio; + serviceRegistry = _serviceRegistry; + } + + function _checkTokenSecurityDeposit(uint256 serviceId) internal view virtual {} + + function _withdraw(address to, uint256 amount) internal virtual {} + + function stake(uint256 serviceId) external { + // Check the service conditions for staking + (, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); + // The service must be deployed + if (state != 4) { + revert(); + } + // Check the service token, if applicable + _checkTokenSecurityDeposit(serviceId); + + // Transfer the service for staking + IService(serviceRegistry).transferFrom(msg.sender, address(this), serviceId); + + // ServiceInfo struct will be an empty one since otherwise the transferFrom above would fail + ServiceInfo storage sInfo = mapServiceInfo[serviceId]; + sInfo.multisig = multisig; + sInfo.owner = msg.sender; + sInfo.nonce = IMultisig(multisig).nonce(); + sInfo.ts = block.timestamp; + + // Add the service Id to the set of staked services + setServiceIds.push(serviceId); + } + + function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { + uint256 size = setServiceIds.length; + uint256 numServices; + uint256[] memory eligibleServiceIds = new uint256[](size); + + // Calculate each staked service reward eligibility + for (uint256 i = 0; i < size; ++i) { + // Get the current service Id + uint256 curServiceId = setServiceIds[i]; + + // Get the service info + ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; + + // Calculate the staking nonce ratio + uint256 curNonce = IMultisig(curInfo.multisig).nonce(); + // Calculate the nonce ratio in 1e18 value + uint256 ratio = ((block.timestamp - curInfo.ts) * 1e18) / (curNonce - curInfo.nonce); + + // Record the reward for the service if it has provided enough transactions + if (ratio >= stakingRatio) { + eligibleServiceIds[numServices] = curServiceId; + ++numServices; + } else { + // Record a current timestamp for each service + curInfo.ts = block.timestamp; + } + + // Record the unstaked service Id index in the global set of staked service Ids + if (curServiceId == serviceId) { + idx = i; + } + } + + // Calculate each eligible service Id reward + uint256 tsLastBalance = tsLastDeposit; + uint256 totalReward; + for (uint256 i = 0; i < numServices; ++i) { + uint256 curServiceId = eligibleServiceIds[i]; + ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; + + // If the staking was longer than the deposited period, adjust that amount + if (curInfo.ts < tsLastBalance) { + curInfo.ts = tsLastBalance; + } + + // Calculate the reward up until now + uint256 reward = (balance * apy * 365 days) / ((block.timestamp - curInfo.ts) * numServices); + // Add the reward + curInfo.reward += reward; + totalReward += reward; + + // Adjust the starting ts for each service to a current timestamp + curInfo.ts = block.timestamp; + } + + // Adjust the deposit balance + balance -= totalReward; + + // Record the current timestamp as the one to make future staking calculations from + tsLastDeposit = block.timestamp; + } + + function unstake(uint256 serviceId) external { + ServiceInfo storage sInfo = mapServiceInfo[serviceId]; + if (msg.sender != sInfo.owner) { + revert(); + } + + uint256 idx = _checkpoint(serviceId); + + // Transfer the service back to the owner + IService(serviceRegistry).transferFrom(address(this), msg.sender, serviceId); + + // Send the remaining small balance along with the reward if it is below the chosen threshold + uint256 amount = sInfo.reward; + uint256 curBalance = balance; + if (curBalance < minBalance) { + amount += curBalance; + balance = 0; + } + + // Transfer accumulated rewards to the service owner + _withdraw(msg.sender, amount); + + // Clear all the data about the unstaked service + // Delete the service info struct + delete mapServiceInfo[serviceId]; + + // Update the set of staked service Ids + setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; + setServiceIds.pop(); + } +} \ No newline at end of file diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol new file mode 100644 index 00000000..a45663d6 --- /dev/null +++ b/contracts/staking/ServiceStakingToken.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "./ServiceStakingBase.sol"; + +interface IToken { + function transferFrom(address from, address to, uint256 id) external returns (bool); +} + +interface IServiceTokenUtility { + function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); +} + +/// @dev Failure of a transfer. +/// @param token Address of a token. +/// @param from Address `from`. +/// @param to Address `to`. +/// @param value Value. +error TransferFailed(address token, address from, address to, uint256 value); + +/// @title ServiceStakingToken - Smart contract for staking the service by its owner having an ERC20 token as a deposit +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +contract ServiceStakingToken is ServiceStakingBase { + // ServiceRegistryTokenUtility address + address public immutable serviceRegistryTokenUtility; + // Security token address for staking corresponding to the service deposit token + address public immutable securityToken; + + constructor( + uint256 _apy, + uint256 _minServiceDeposit, + uint256 _stakingRatio, + address _serviceRegistry, + address _serviceRegistryTokenUtility, + address _securityToken + ) + ServiceStakingBase(_apy, _minServiceDeposit, _stakingRatio, _serviceRegistry) + { + securityToken = _securityToken; + serviceRegistryTokenUtility = _serviceRegistryTokenUtility; + } + + /// @dev Safe token transferFrom implementation. + /// @notice The implementation is fully copied from the audited MIT-licensed solmate code repository: + /// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol + /// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract + /// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. + /// Also, the final `require` statement is modified with this contract own `revert` statement. + /// @param token Token address. + /// @param from Address to transfer tokens from. + /// @param to Address to transfer tokens to. + /// @param amount Token amount. + function safeTransferFrom(address token, address from, address to, uint256 amount) internal { + bool success; + + // solhint-disable-next-line no-inline-assembly + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000) + mstore(4, from) // Append the "from" argument. + mstore(36, to) // Append the "to" argument. + mstore(68, amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 100 because that's the total length of our calldata (4 + 32 * 3) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0, 100, 0, 32) + ) + + mstore(0x60, 0) // Restore the zero slot to zero. + mstore(0x40, memPointer) // Restore the memPointer. + } + + if (!success) { + revert TransferFailed(token, from, to, amount); + } + } + + /// @dev Safe token transfer implementation. + /// @notice The implementation is fully copied from the audited MIT-licensed solmate code repository: + /// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol + /// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract + /// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. + /// Also, the final `require` statement is modified with this contract own `revert` statement. + /// @param token Token address. + /// @param to Address to transfer tokens to. + /// @param amount Token amount. + function safeTransfer(address token, address to, uint256 amount) internal { + bool success; + + // solhint-disable-next-line no-inline-assembly + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(4, to) // Append the "to" argument. + mstore(36, amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 68 because that's the total length of our calldata (4 + 32 * 2) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0, 68, 0, 32) + ) + + mstore(0x60, 0) // Restore the zero slot to zero. + mstore(0x40, memPointer) // Restore the memPointer. + } + + if (!success) { + revert TransferFailed(token, address(this), to, amount); + } + } + + function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { + // Get the service security token and deposit + (address token, uint96 securityDeposit) = + IServiceTokenUtility(serviceRegistryTokenUtility).mapServiceIdTokenDeposit(serviceId); + + // The security token must match the contract token + if (securityToken != token) { + revert(); + } + + // The security deposit must be greater or equal to the minimum defined one + if (securityDeposit < minSecurityDeposit) { + revert(); + } + } + + function _withdraw(address to, uint256 amount) internal override { + safeTransfer(securityToken, to, amount); + } + + function deposit(uint256 amount) external { + // Distribute current staking rewards + _checkpoint(0); + + // Add to the overall balance + safeTransferFrom(securityToken, msg.sender, address(this), amount); + balance += amount; + } +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index 072d1b45..62c04a4b 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -102,7 +102,7 @@ module.exports = { solidity: { compilers: [ { - version: "0.8.19", + version: "0.8.21", settings: { optimizer: { enabled: true, From 54abdab33e26ef7edec4ec1c8fd2b1731696a875 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 22 Sep 2023 10:44:25 +0100 Subject: [PATCH 02/16] refactor: continue working on the service staking implementation --- contracts/staking/ServiceStakingBase.sol | 112 +++++++++++++---------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 28c1a525..139d4510 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -52,7 +52,7 @@ abstract contract ServiceStakingBase { // Token / ETH balance uint256 public balance; // Timestamp of the last checkpoint - uint256 public tsLastDeposit; + uint256 public tsLastBalance; // Minimum balance going below which would be considered that the balance is zero uint256 public minBalance; // Mapping of serviceId => staking service info @@ -96,65 +96,77 @@ abstract contract ServiceStakingBase { } function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { + // Get the uint256 size = setServiceIds.length; - uint256 numServices; - uint256[] memory eligibleServiceIds = new uint256[](size); - - // Calculate each staked service reward eligibility - for (uint256 i = 0; i < size; ++i) { - // Get the current service Id - uint256 curServiceId = setServiceIds[i]; - - // Get the service info - ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; - - // Calculate the staking nonce ratio - uint256 curNonce = IMultisig(curInfo.multisig).nonce(); - // Calculate the nonce ratio in 1e18 value - uint256 ratio = ((block.timestamp - curInfo.ts) * 1e18) / (curNonce - curInfo.nonce); - - // Record the reward for the service if it has provided enough transactions - if (ratio >= stakingRatio) { - eligibleServiceIds[numServices] = curServiceId; - ++numServices; - } else { - // Record a current timestamp for each service - curInfo.ts = block.timestamp; - } + uint256 lastBalance = balance; - // Record the unstaked service Id index in the global set of staked service Ids - if (curServiceId == serviceId) { - idx = i; + // If the balance is zero, just bump the timestamp for each of the staking service + if (lastBalance == 0) { + for (uint256 i = 0; i < size; ++i) { + // Set the current timestamp + mapServiceInfo[setServiceIds[i]].ts = block.timestamp; + } + } else { + uint256 numServices; + uint256[] memory eligibleServiceIds = new uint256[](size); + + // Calculate each staked service reward eligibility + for (uint256 i = 0; i < size; ++i) { + // Get the current service Id + uint256 curServiceId = setServiceIds[i]; + + // Get the service info + ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; + + // Calculate the staking nonce ratio + uint256 curNonce = IMultisig(curInfo.multisig).nonce(); + // Calculate the nonce ratio in 1e18 value + uint256 ratio = ((block.timestamp - curInfo.ts) * 1e18) / (curNonce - curInfo.nonce); + + // Record the reward for the service if it has provided enough transactions + if (ratio >= stakingRatio) { + eligibleServiceIds[numServices] = curServiceId; + ++numServices; + } else { + // Record current timestamp for each reward ineligible service + curInfo.ts = block.timestamp; + } + // Record current service multisig nonce + curInfo.nonce = curNonce; + + // Record the unstaked service Id index in the global set of staked service Ids + if (curServiceId == serviceId) { + idx = i; + } } - } - // Calculate each eligible service Id reward - uint256 tsLastBalance = tsLastDeposit; - uint256 totalReward; - for (uint256 i = 0; i < numServices; ++i) { - uint256 curServiceId = eligibleServiceIds[i]; - ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; + // Calculate each eligible service Id reward + uint256 totalReward; + for (uint256 i = 0; i < numServices; ++i) { + uint256 curServiceId = eligibleServiceIds[i]; + ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; - // If the staking was longer than the deposited period, adjust that amount - if (curInfo.ts < tsLastBalance) { - curInfo.ts = tsLastBalance; - } - // Calculate the reward up until now - uint256 reward = (balance * apy * 365 days) / ((block.timestamp - curInfo.ts) * numServices); - // Add the reward - curInfo.reward += reward; - totalReward += reward; - // Adjust the starting ts for each service to a current timestamp - curInfo.ts = block.timestamp; - } + // Calculate the reward up until now + // If the staking was longer than the deposited period, the service's timestamp is adjusted such that + // it is equal to at most the tsLastBalance of the last deposit happening during every _checkpoint() call + uint256 reward = (lastBalance * apy * (block.timestamp - curInfo.ts)) / (365 days * numServices * 100); + // Add the reward + curInfo.reward += reward; + totalReward += reward; - // Adjust the deposit balance - balance -= totalReward; + // Adjust the starting ts for each service to a current timestamp + curInfo.ts = block.timestamp; + } + + // Adjust the deposit balance + lastBalance -= totalReward; + balance = lastBalance; + } // Record the current timestamp as the one to make future staking calculations from - tsLastDeposit = block.timestamp; + tsLastBalance = block.timestamp; } function unstake(uint256 serviceId) external { From d86309205a0c5952efa35b4588b40ba63e3d98af Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 22 Sep 2023 16:50:20 +0100 Subject: [PATCH 03/16] refactor: advancing service staking contracts --- .gitleaksignore | 1 + contracts/staking/ServiceStaking.sol | 9 ++++- contracts/staking/ServiceStakingBase.sol | 42 ++++++++++++++++------- contracts/staking/ServiceStakingToken.sol | 11 +++++- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.gitleaksignore b/.gitleaksignore index ddbb9e55..e0ac0c9b 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -41,3 +41,4 @@ d780294b8dccf047391a71b75dade50a6b89003b:scripts/deployment/l2/globals_gnosis_ch 7d2fbdb1556dc3ed289edf7e116b378b6f6a9d83:scripts/deployment/l2/bridges/gnosis/test_service_registry_token_utility_change_drainer.js:generic-api-key:44 7bb76e80382eff3c538af340289ed8241f4e4551:scripts/deployment/l2/globals_gnosis_mainnet.json:generic-api-key:1 7bb76e80382eff3c538af340289ed8241f4e4551:scripts/deployment/l2/globals_gnosis_mainnet.json:generic-api-key:2 +233a57ccda8d5d84a86133422daa99807801d9fc:scripts/deployment/l2/globals_gnosis_mainnet.json:generic-api-key:2 diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index 2a5cbd04..cddfe225 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -13,6 +13,7 @@ error TransferFailed(address token, address from, address to, uint256 value); /// @title ServiceStakingToken - Smart contract for staking the service by its owner based ETH as a deposit /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - contract ServiceStaking is ServiceStakingBase { constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) @@ -40,6 +41,12 @@ contract ServiceStaking is ServiceStakingBase { _checkpoint(0); // Add to the overall balance - balance += msg.value; + uint256 newBalance = balance + msg.value; + + // Update rewards per second + rewardsPerSecond = (newBalance * apy) / (100 * 365 days); + + // Record the new actual balance + balance = newBalance; } } \ No newline at end of file diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 139d4510..b44764f6 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -39,6 +39,7 @@ struct ServiceInfo { /// @title ServiceStakingBase - Base abstract smart contract for staking the service by its owner /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - abstract contract ServiceStakingBase { // APY value uint256 public immutable apy; @@ -55,6 +56,8 @@ abstract contract ServiceStakingBase { uint256 public tsLastBalance; // Minimum balance going below which would be considered that the balance is zero uint256 public minBalance; + // Rewards per second + uint256 public rewardsPerSecond; // Mapping of serviceId => staking service info mapping (uint256 => ServiceInfo) public mapServiceInfo; // Set of currently staking serviceIds @@ -78,7 +81,7 @@ abstract contract ServiceStakingBase { if (state != 4) { revert(); } - // Check the service token, if applicable + // Check the service security deposit and token, if applicable _checkTokenSecurityDeposit(serviceId); // Transfer the service for staking @@ -141,27 +144,40 @@ abstract contract ServiceStakingBase { } // Calculate each eligible service Id reward - uint256 totalReward; + uint256 tsLast = tsLastBalance; + // Calculate the maximum possible reward per service during the last deposit period + uint256 maxRewardsPerService = (rewardsPerSecond * (block.timestamp - tsLast)) / numServices; + // Traverse all the eligible services and calculate their rewards for (uint256 i = 0; i < numServices; ++i) { uint256 curServiceId = eligibleServiceIds[i]; ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; - - // Calculate the reward up until now // If the staking was longer than the deposited period, the service's timestamp is adjusted such that // it is equal to at most the tsLastBalance of the last deposit happening during every _checkpoint() call - uint256 reward = (lastBalance * apy * (block.timestamp - curInfo.ts)) / (365 days * numServices * 100); + uint256 reward = rewardsPerSecond * (block.timestamp - curInfo.ts); + // Adjust the reward if it goes out of calculated max bounds + if (reward > maxRewardsPerService) { + reward = maxRewardsPerService; + } + + // Adjust the deposit balance + if (lastBalance >= reward) { + lastBalance -= reward; + } else { + // This situation must never happen + // TODO: Fuzz this + reward = lastBalance; + lastBalance = 0; + } + // Add the reward curInfo.reward += reward; - totalReward += reward; // Adjust the starting ts for each service to a current timestamp curInfo.ts = block.timestamp; } - // Adjust the deposit balance - lastBalance -= totalReward; balance = lastBalance; } @@ -182,14 +198,16 @@ abstract contract ServiceStakingBase { // Send the remaining small balance along with the reward if it is below the chosen threshold uint256 amount = sInfo.reward; - uint256 curBalance = balance; - if (curBalance < minBalance) { - amount += curBalance; + uint256 lastBalance = balance; + if (lastBalance < minBalance) { + amount += lastBalance; balance = 0; } // Transfer accumulated rewards to the service owner - _withdraw(msg.sender, amount); + if (amount > 0) { + _withdraw(msg.sender, amount); + } // Clear all the data about the unstaked service // Delete the service info struct diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index a45663d6..af5ee6c6 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -21,6 +21,7 @@ error TransferFailed(address token, address from, address to, uint256 value); /// @title ServiceStakingToken - Smart contract for staking the service by its owner having an ERC20 token as a deposit /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - +/// @author Mariapia Moscatiello - contract ServiceStakingToken is ServiceStakingBase { // ServiceRegistryTokenUtility address address public immutable serviceRegistryTokenUtility; @@ -151,6 +152,14 @@ contract ServiceStakingToken is ServiceStakingBase { // Add to the overall balance safeTransferFrom(securityToken, msg.sender, address(this), amount); - balance += amount; + + // Add to the overall balance + uint256 newBalance = balance + amount; + + // Update rewards per second + rewardsPerSecond = (newBalance * apy) / (100 * 365 days); + + // Record the new actual balance + balance = newBalance; } } \ No newline at end of file From 8cc3855efcb0d5558c0c10c8663d3a248072de30 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Fri, 22 Sep 2023 18:31:12 +0100 Subject: [PATCH 04/16] refactor: advancing service staking contracts --- contracts/staking/ServiceStaking.sol | 19 ++++++- contracts/staking/ServiceStakingBase.sol | 68 +++++++++++++++++++++-- contracts/staking/ServiceStakingToken.sol | 42 ++++++++++++-- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index cddfe225..57f9eb31 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -15,10 +15,19 @@ error TransferFailed(address token, address from, address to, uint256 value); /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - contract ServiceStaking is ServiceStakingBase { + /// @dev ServiceStaking constructor. + /// @param _apy Staking APY (in single digits). + /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _serviceRegistry ServiceRegistry contract address. constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) - {} + { + // TODO: calculate minBalance + } + /// @dev Checks token security deposit. + /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { // Get the service security token and deposit (uint96 securityDeposit, , , , , , ) = IService(serviceRegistry).mapServices(serviceId); @@ -29,6 +38,9 @@ contract ServiceStaking is ServiceStakingBase { } } + /// @dev Withdraws the reward amount to a service owner. + /// @param to Address to. + /// @param amount Amount to withdraw. function _withdraw(address to, uint256 amount) internal override { (bool result, ) = to.call{value: amount}(""); if (!result) { @@ -44,9 +56,12 @@ contract ServiceStaking is ServiceStakingBase { uint256 newBalance = balance + msg.value; // Update rewards per second - rewardsPerSecond = (newBalance * apy) / (100 * 365 days); + uint256 newRewardsPerSecond = (newBalance * apy) / (100 * 365 days); + rewardsPerSecond = newRewardsPerSecond; // Record the new actual balance balance = newBalance; + + emit Deposit(msg.sender, msg.value, newBalance, newRewardsPerSecond); } } \ No newline at end of file diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index b44764f6..b0c7ead2 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -1,11 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; +// Multisig interface interface IMultisig { + /// @dev Gets the multisig nonce. + /// @return Multisig nonce. function nonce() external returns (uint256); } +// Service Registry interface interface IService { + /// @dev Transfers the service that was previously approved to this contract address. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param id Service Id. function transferFrom(address from, address to, uint256 id) external; /// @dev Gets the service instance from the map of services. @@ -28,11 +36,23 @@ interface IService { ); } +/// @dev Provided zero value. +error ZeroValue(); + +/// @dev Provided zero address. +error ZeroAddress(); + +// Service Info struct struct ServiceInfo { + // Service multisig address address multisig; + // Service owner address owner; + // Service multisig nonce uint256 nonce; + // Staking time uint256 ts; + // Accumulated service staking reward uint256 reward; } @@ -41,6 +61,12 @@ struct ServiceInfo { /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - abstract contract ServiceStakingBase { + event ServiceStaked(uint256 indexed serviceId, address indexed owner); + event Checkpoint(uint256 indexed balance); + event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward); + event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 rewardsPerSecond); + event Withdraw(address indexed to, uint256 amount); + // APY value uint256 public immutable apy; // Minimum deposit value for staking @@ -54,7 +80,7 @@ abstract contract ServiceStakingBase { uint256 public balance; // Timestamp of the last checkpoint uint256 public tsLastBalance; - // Minimum balance going below which would be considered that the balance is zero + // Minimum balance going below which would be given away, such that the contract balance is set to zero uint256 public minBalance; // Rewards per second uint256 public rewardsPerSecond; @@ -63,17 +89,37 @@ abstract contract ServiceStakingBase { // Set of currently staking serviceIds uint256[] public setServiceIds; + /// @dev ServiceStakingBase constructor. + /// @param _apy Staking APY (in single digits). + /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _serviceRegistry ServiceRegistry contract address. constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) { + // Initial checks + if (_apy == 0 || _minSecurityDeposit == 0 || _stakingRatio == 0) { + revert ZeroValue(); + } + if (_serviceRegistry == address(0)) { + revert ZeroAddress(); + } + apy = _apy; minSecurityDeposit = _minSecurityDeposit; stakingRatio = _stakingRatio; serviceRegistry = _serviceRegistry; } + /// @dev Checks token security deposit. + /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view virtual {} + /// @dev Withdraws the reward amount to a service owner. + /// @param to Address to. + /// @param amount Amount to withdraw. function _withdraw(address to, uint256 amount) internal virtual {} + /// @dev Stakes the service. + /// @param serviceId Service Id. function stake(uint256 serviceId) external { // Check the service conditions for staking (, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); @@ -96,10 +142,14 @@ abstract contract ServiceStakingBase { // Add the service Id to the set of staked services setServiceIds.push(serviceId); + + emit ServiceStaked(serviceId, msg.sender); } + /// @dev Checkpoint to allocate rewards up until a current time. + /// @param serviceId Service Id that unstakes, or 0 if the function is called during the deposit of new funds. function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { - // Get the + // Get the service Id set length uint256 size = setServiceIds.length; uint256 lastBalance = balance; @@ -171,26 +221,32 @@ abstract contract ServiceStakingBase { lastBalance = 0; } - // Add the reward + // Add the calculated reward to the service info curInfo.reward += reward; // Adjust the starting ts for each service to a current timestamp curInfo.ts = block.timestamp; } - + // Update the storage balance balance = lastBalance; } - // Record the current timestamp as the one to make future staking calculations from + // Record the current timestamp such that next calculations start from this point of time tsLastBalance = block.timestamp; + + emit Checkpoint(lastBalance); } + /// @dev Unstakes the service. + /// @param serviceId Service Id. function unstake(uint256 serviceId) external { ServiceInfo storage sInfo = mapServiceInfo[serviceId]; + // Check for the service ownership if (msg.sender != sInfo.owner) { revert(); } + // Call the checkpoint and get the service index in the set of services uint256 idx = _checkpoint(serviceId); // Transfer the service back to the owner @@ -216,5 +272,7 @@ abstract contract ServiceStakingBase { // Update the set of staked service Ids setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; setServiceIds.pop(); + + emit ServiceUnstaked(serviceId, msg.sender, amount); } } \ No newline at end of file diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index af5ee6c6..97178122 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -4,10 +4,19 @@ pragma solidity ^0.8.21; import "./ServiceStakingBase.sol"; interface IToken { - function transferFrom(address from, address to, uint256 id) external returns (bool); + /// @dev Transfers the token amount that was previously approved up until the maximum allowance. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param amount Amount to transfer to. + /// @return True if the function execution is successful. + function transferFrom(address from, address to, uint256 amount) external returns (bool); } interface IServiceTokenUtility { + /// @dev Gets the service security token info. + /// @param serviceId Service Id. + /// @return Token address. + /// @return Token security deposit. function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); } @@ -28,16 +37,29 @@ contract ServiceStakingToken is ServiceStakingBase { // Security token address for staking corresponding to the service deposit token address public immutable securityToken; + /// @dev ServiceStakingToken constructor. + /// @param _apy Staking APY (in single digits). + /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _serviceRegistry ServiceRegistry contract address. + /// @param _serviceRegistryTokenUtility ServiceRegistryTokenUtility contract address. + /// @param _securityToken Address of a service security token. constructor( uint256 _apy, - uint256 _minServiceDeposit, + uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry, address _serviceRegistryTokenUtility, address _securityToken ) - ServiceStakingBase(_apy, _minServiceDeposit, _stakingRatio, _serviceRegistry) + ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) { + // TODO: calculate minBalance + // Initial checks + if (_securityToken == address(0) || _serviceRegistryTokenUtility == address(0)) { + revert ZeroAddress(); + } + securityToken = _securityToken; serviceRegistryTokenUtility = _serviceRegistryTokenUtility; } @@ -126,6 +148,8 @@ contract ServiceStakingToken is ServiceStakingBase { } } + /// @dev Checks token security deposit. + /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { // Get the service security token and deposit (address token, uint96 securityDeposit) = @@ -142,10 +166,17 @@ contract ServiceStakingToken is ServiceStakingBase { } } + /// @dev Withdraws the reward amount to a service owner. + /// @param to Address to. + /// @param amount Amount to withdraw. function _withdraw(address to, uint256 amount) internal override { safeTransfer(securityToken, to, amount); + + emit Withdraw(to, amount); } + /// @dev Deposits funds for staking. + /// @param amount Token amount to deposit. function deposit(uint256 amount) external { // Distribute current staking rewards _checkpoint(0); @@ -157,9 +188,12 @@ contract ServiceStakingToken is ServiceStakingBase { uint256 newBalance = balance + amount; // Update rewards per second - rewardsPerSecond = (newBalance * apy) / (100 * 365 days); + uint256 newRewardsPerSecond = (newBalance * apy) / (100 * 365 days); + rewardsPerSecond = newRewardsPerSecond; // Record the new actual balance balance = newBalance; + + emit Deposit(msg.sender, amount, newBalance, newRewardsPerSecond); } } \ No newline at end of file From 4d2112cfa2e26c50f6d5b86a8b3574b8971f4414 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Mon, 25 Sep 2023 13:55:56 +0100 Subject: [PATCH 05/16] refactor: separate interfaces, separate balance accounting and record times of staking --- contracts/interfaces/IMultisig.sol | 4 + contracts/interfaces/IService.sol | 27 +++- contracts/interfaces/IServiceTokenUtility.sol | 6 + contracts/interfaces/IToken.sol | 44 ++++++ contracts/staking/ServiceStaking.sol | 26 ++-- contracts/staking/ServiceStakingBase.sol | 138 +++++++---------- contracts/staking/ServiceStakingToken.sol | 146 +++--------------- contracts/utils/SafeTransferLib.sol | 95 ++++++++++++ 8 files changed, 265 insertions(+), 221 deletions(-) create mode 100644 contracts/interfaces/IToken.sol create mode 100644 contracts/utils/SafeTransferLib.sol diff --git a/contracts/interfaces/IMultisig.sol b/contracts/interfaces/IMultisig.sol index bc1034ff..f09707fd 100644 --- a/contracts/interfaces/IMultisig.sol +++ b/contracts/interfaces/IMultisig.sol @@ -13,4 +13,8 @@ interface IMultisig { uint256 threshold, bytes memory data ) external returns (address multisig); + + /// @dev Gets the multisig nonce. + /// @return Multisig nonce. + function nonce() external returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/IService.sol b/contracts/interfaces/IService.sol index 08d16ce3..0d7e3a5a 100644 --- a/contracts/interfaces/IService.sol +++ b/contracts/interfaces/IService.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.15; /// @dev Required interface for the service manipulation. -interface IService{ +interface IService { struct AgentParams { // Number of agent instances uint32 slots; @@ -87,4 +87,29 @@ interface IService{ /// @return success True, if function executed successfully. /// @return refund The amount of refund returned to the operator. function unbond(address operator, uint256 serviceId) external returns (bool success, uint256 refund); + + /// @dev Transfers the service that was previously approved to this contract address. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param id Service Id. + function transferFrom(address from, address to, uint256 id) external; + + /// @dev Gets the service instance from the map of services. + /// @param serviceId Service Id. + /// @return securityDeposit Registration activation deposit. + /// @return multisig Service multisig address. + /// @return configHash IPFS hashes pointing to the config metadata. + /// @return threshold Agent instance signers threshold. + /// @return maxNumAgentInstances Total number of agent instances. + /// @return numAgentInstances Actual number of agent instances. + /// @return state Service state. + function mapServices(uint256 serviceId) external view returns ( + uint96 securityDeposit, + address multisig, + bytes32 configHash, + uint32 threshold, + uint32 maxNumAgentInstances, + uint32 numAgentInstances, + uint8 state + ); } diff --git a/contracts/interfaces/IServiceTokenUtility.sol b/contracts/interfaces/IServiceTokenUtility.sol index d8fbdbaf..e337bdba 100644 --- a/contracts/interfaces/IServiceTokenUtility.sol +++ b/contracts/interfaces/IServiceTokenUtility.sol @@ -50,4 +50,10 @@ interface IServiceTokenUtility { /// @param serviceId Service Id. /// @return True if the service Id is token secured. function isTokenSecuredService(uint256 serviceId) external view returns (bool); + + /// @dev Gets the service security token info. + /// @param serviceId Service Id. + /// @return Token address. + /// @return Token security deposit. + function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); } diff --git a/contracts/interfaces/IToken.sol b/contracts/interfaces/IToken.sol new file mode 100644 index 00000000..813a8b08 --- /dev/null +++ b/contracts/interfaces/IToken.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/// @dev Generic token interface for IERC20 and IERC721 tokens. +interface IToken { + /// @dev Gets the amount of tokens owned by a specified account. + /// @param account Account address. + /// @return Amount of tokens owned. + function balanceOf(address account) external view returns (uint256); + + /// @dev Gets the owner of the token Id. + /// @param tokenId Token Id. + /// @return Token Id owner address. + function ownerOf(uint256 tokenId) external view returns (address); + + /// @dev Gets the total amount of tokens stored by the contract. + /// @return Amount of tokens. + function totalSupply() external view returns (uint256); + + /// @dev Transfers the token amount. + /// @param to Address to transfer to. + /// @param amount The amount to transfer. + /// @return True if the function execution is successful. + function transfer(address to, uint256 amount) external returns (bool); + + /// @dev Gets remaining number of tokens that the `spender` can transfer on behalf of `owner`. + /// @param owner Token owner. + /// @param spender Account address that is able to transfer tokens on behalf of the owner. + /// @return Token amount allowed to be transferred. + function allowance(address owner, address spender) external view returns (uint256); + + /// @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + /// @param spender Account address that will be able to transfer tokens on behalf of the caller. + /// @param amount Token amount. + /// @return True if the function execution is successful. + function approve(address spender, uint256 amount) external returns (bool); + + /// @dev Transfers the token amount that was previously approved up until the maximum allowance. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param amount Amount to transfer to. + /// @return True if the function execution is successful. + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index 57f9eb31..52882704 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -1,16 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -import "./ServiceStakingBase.sol"; +import {ServiceStakingBase} from "./ServiceStakingBase.sol"; +import "../interfaces/IService.sol"; -/// @dev Failure of a transfer. -/// @param token Address of a token. -/// @param from Address `from`. -/// @param to Address `to`. -/// @param value Value. -error TransferFailed(address token, address from, address to, uint256 value); - -/// @title ServiceStakingToken - Smart contract for staking the service by its owner based ETH as a deposit +/// @title ServiceStakingToken - Smart contract for staking a service by its owner when the service has an ETH as the deposit /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - @@ -42,6 +36,10 @@ contract ServiceStaking is ServiceStakingBase { /// @param to Address to. /// @param amount Amount to withdraw. function _withdraw(address to, uint256 amount) internal override { + // Update the contract balance + balance -= amount; + + // Transfer the amount (bool result, ) = to.call{value: amount}(""); if (!result) { revert TransferFailed(address(0), address(this), to, amount); @@ -52,16 +50,18 @@ contract ServiceStaking is ServiceStakingBase { // Distribute current staking rewards _checkpoint(0); - // Add to the overall balance + // Add to the contract and available rewards balances uint256 newBalance = balance + msg.value; + uint256 newAvailableRewards = availableRewards + msg.value; // Update rewards per second - uint256 newRewardsPerSecond = (newBalance * apy) / (100 * 365 days); + uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); rewardsPerSecond = newRewardsPerSecond; - // Record the new actual balance + // Record the new actual balance and available rewards balance = newBalance; + availableRewards = newAvailableRewards; - emit Deposit(msg.sender, msg.value, newBalance, newRewardsPerSecond); + emit Deposit(msg.sender, msg.value, newBalance, newAvailableRewards, newRewardsPerSecond); } } \ No newline at end of file diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index b0c7ead2..ffa2cbb6 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -1,46 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -// Multisig interface -interface IMultisig { - /// @dev Gets the multisig nonce. - /// @return Multisig nonce. - function nonce() external returns (uint256); -} - -// Service Registry interface -interface IService { - /// @dev Transfers the service that was previously approved to this contract address. - /// @param from Account address to transfer from. - /// @param to Account address to transfer to. - /// @param id Service Id. - function transferFrom(address from, address to, uint256 id) external; - - /// @dev Gets the service instance from the map of services. - /// @param serviceId Service Id. - /// @return securityDeposit Registration activation deposit. - /// @return multisig Service multisig address. - /// @return configHash IPFS hashes pointing to the config metadata. - /// @return threshold Agent instance signers threshold. - /// @return maxNumAgentInstances Total number of agent instances. - /// @return numAgentInstances Actual number of agent instances. - /// @return state Service state. - function mapServices(uint256 serviceId) external view returns ( - uint96 securityDeposit, - address multisig, - bytes32 configHash, - uint32 threshold, - uint32 maxNumAgentInstances, - uint32 numAgentInstances, - uint8 state - ); -} - -/// @dev Provided zero value. -error ZeroValue(); - -/// @dev Provided zero address. -error ZeroAddress(); +import "../interfaces/IErrorsRegistries.sol"; +import "../interfaces/IMultisig.sol"; +import "../interfaces/IService.sol"; // Service Info struct struct ServiceInfo { @@ -50,21 +13,22 @@ struct ServiceInfo { address owner; // Service multisig nonce uint256 nonce; - // Staking time - uint256 ts; + // Staking start time + uint256 tsStart; // Accumulated service staking reward uint256 reward; } -/// @title ServiceStakingBase - Base abstract smart contract for staking the service by its owner +/// @title ServiceStakingBase - Base abstract smart contract for staking a service by its owner /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - -abstract contract ServiceStakingBase { +abstract contract ServiceStakingBase is IErrorsRegistries { event ServiceStaked(uint256 indexed serviceId, address indexed owner); event Checkpoint(uint256 indexed balance); event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward); - event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 rewardsPerSecond); + event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 newAvailableRewards, + uint256 rewardsPerSecond); event Withdraw(address indexed to, uint256 amount); // APY value @@ -78,8 +42,10 @@ abstract contract ServiceStakingBase { // Token / ETH balance uint256 public balance; + // Token / ETH available rewards + uint256 public availableRewards; // Timestamp of the last checkpoint - uint256 public tsLastBalance; + uint256 public tsCheckpoint; // Minimum balance going below which would be given away, such that the contract balance is set to zero uint256 public minBalance; // Rewards per second @@ -125,7 +91,7 @@ abstract contract ServiceStakingBase { (, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); // The service must be deployed if (state != 4) { - revert(); + revert WrongServiceState(state, serviceId); } // Check the service security deposit and token, if applicable _checkTokenSecurityDeposit(serviceId); @@ -138,7 +104,7 @@ abstract contract ServiceStakingBase { sInfo.multisig = multisig; sInfo.owner = msg.sender; sInfo.nonce = IMultisig(multisig).nonce(); - sInfo.ts = block.timestamp; + sInfo.tsStart = block.timestamp; // Add the service Id to the set of staked services setServiceIds.push(serviceId); @@ -151,15 +117,12 @@ abstract contract ServiceStakingBase { function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { // Get the service Id set length uint256 size = setServiceIds.length; - uint256 lastBalance = balance; + uint256 lastAvailableRewards = availableRewards; + uint256 tsCheckpointLast = tsCheckpoint; - // If the balance is zero, just bump the timestamp for each of the staking service - if (lastBalance == 0) { - for (uint256 i = 0; i < size; ++i) { - // Set the current timestamp - mapServiceInfo[setServiceIds[i]].ts = block.timestamp; - } - } else { + // If available rewards are not zero, proceed with staking calculation + // Otherwise, just bump the timestamp of last checkpoint + if (lastAvailableRewards > 0) { uint256 numServices; uint256[] memory eligibleServiceIds = new uint256[](size); @@ -173,17 +136,21 @@ abstract contract ServiceStakingBase { // Calculate the staking nonce ratio uint256 curNonce = IMultisig(curInfo.multisig).nonce(); + // Get the last service checkpoint: staking start time or the global checkpoint timestamp + uint256 serviceCheckpoint = tsCheckpointLast; + // Adjust the service checkpoint time if the service was staking less than the current staking period + if (curInfo.tsStart > tsCheckpointLast) { + serviceCheckpoint = curInfo.tsStart; + } // Calculate the nonce ratio in 1e18 value - uint256 ratio = ((block.timestamp - curInfo.ts) * 1e18) / (curNonce - curInfo.nonce); + uint256 ratio = ((block.timestamp - serviceCheckpoint) * 1e18) / (curNonce - curInfo.nonce); // Record the reward for the service if it has provided enough transactions if (ratio >= stakingRatio) { eligibleServiceIds[numServices] = curServiceId; ++numServices; - } else { - // Record current timestamp for each reward ineligible service - curInfo.ts = block.timestamp; } + // Record current service multisig nonce curInfo.nonce = curNonce; @@ -193,48 +160,51 @@ abstract contract ServiceStakingBase { } } - // Calculate each eligible service Id reward - uint256 tsLast = tsLastBalance; + // Process each eligible service Id reward // Calculate the maximum possible reward per service during the last deposit period - uint256 maxRewardsPerService = (rewardsPerSecond * (block.timestamp - tsLast)) / numServices; + uint256 maxRewardsPerService = (rewardsPerSecond * (block.timestamp - tsCheckpointLast)) / numServices; // Traverse all the eligible services and calculate their rewards for (uint256 i = 0; i < numServices; ++i) { uint256 curServiceId = eligibleServiceIds[i]; ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; // Calculate the reward up until now + // Get the last service checkpoint: staking start time or the global checkpoint timestamp + uint256 serviceCheckpoint = tsCheckpointLast; + // Adjust the service checkpoint time if the service was staking less than the current staking period + if (curInfo.tsStart > tsCheckpointLast) { + serviceCheckpoint = curInfo.tsStart; + } + // If the staking was longer than the deposited period, the service's timestamp is adjusted such that - // it is equal to at most the tsLastBalance of the last deposit happening during every _checkpoint() call - uint256 reward = rewardsPerSecond * (block.timestamp - curInfo.ts); + // it is equal to at most the tsCheckpoint of the last deposit happening during every _checkpoint() call + uint256 reward = rewardsPerSecond * (block.timestamp - serviceCheckpoint); // Adjust the reward if it goes out of calculated max bounds if (reward > maxRewardsPerService) { reward = maxRewardsPerService; } - // Adjust the deposit balance - if (lastBalance >= reward) { - lastBalance -= reward; + // Adjust the available rewards value + if (lastAvailableRewards >= reward) { + lastAvailableRewards -= reward; } else { // This situation must never happen // TODO: Fuzz this - reward = lastBalance; - lastBalance = 0; + reward = lastAvailableRewards; + lastAvailableRewards = 0; } // Add the calculated reward to the service info curInfo.reward += reward; - - // Adjust the starting ts for each service to a current timestamp - curInfo.ts = block.timestamp; } - // Update the storage balance - balance = lastBalance; + // Update the storage value of available rewards + availableRewards = lastAvailableRewards; } // Record the current timestamp such that next calculations start from this point of time - tsLastBalance = block.timestamp; + tsCheckpoint = block.timestamp; - emit Checkpoint(lastBalance); + emit Checkpoint(lastAvailableRewards); } /// @dev Unstakes the service. @@ -243,7 +213,7 @@ abstract contract ServiceStakingBase { ServiceInfo storage sInfo = mapServiceInfo[serviceId]; // Check for the service ownership if (msg.sender != sInfo.owner) { - revert(); + revert OwnerOnly(msg.sender, sInfo.owner); } // Call the checkpoint and get the service index in the set of services @@ -254,15 +224,15 @@ abstract contract ServiceStakingBase { // Send the remaining small balance along with the reward if it is below the chosen threshold uint256 amount = sInfo.reward; - uint256 lastBalance = balance; - if (lastBalance < minBalance) { - amount += lastBalance; - balance = 0; + uint256 lastAvailableRewards = availableRewards; + if (lastAvailableRewards < minBalance) { + amount += lastAvailableRewards; + availableRewards = 0; } - // Transfer accumulated rewards to the service owner + // Transfer accumulated rewards to the service multisig if (amount > 0) { - _withdraw(msg.sender, amount); + _withdraw(sInfo.multisig, amount); } // Clear all the data about the unstaked service diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index 97178122..0aa865c5 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -1,33 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; -import "./ServiceStakingBase.sol"; - -interface IToken { - /// @dev Transfers the token amount that was previously approved up until the maximum allowance. - /// @param from Account address to transfer from. - /// @param to Account address to transfer to. - /// @param amount Amount to transfer to. - /// @return True if the function execution is successful. - function transferFrom(address from, address to, uint256 amount) external returns (bool); -} - -interface IServiceTokenUtility { - /// @dev Gets the service security token info. - /// @param serviceId Service Id. - /// @return Token address. - /// @return Token security deposit. - function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); -} - -/// @dev Failure of a transfer. -/// @param token Address of a token. -/// @param from Address `from`. -/// @param to Address `to`. -/// @param value Value. -error TransferFailed(address token, address from, address to, uint256 value); - -/// @title ServiceStakingToken - Smart contract for staking the service by its owner having an ERC20 token as a deposit +import {ServiceStakingBase} from "./ServiceStakingBase.sol"; +import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; +import "../interfaces/IToken.sol"; +import "../interfaces/IServiceTokenUtility.sol"; + +/// @title ServiceStakingToken - Smart contract for staking a service by its owner when the service has an ERC20 token as the deposit /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - @@ -35,7 +14,7 @@ contract ServiceStakingToken is ServiceStakingBase { // ServiceRegistryTokenUtility address address public immutable serviceRegistryTokenUtility; // Security token address for staking corresponding to the service deposit token - address public immutable securityToken; + address public immutable stakingToken; /// @dev ServiceStakingToken constructor. /// @param _apy Staking APY (in single digits). @@ -43,111 +22,27 @@ contract ServiceStakingToken is ServiceStakingBase { /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. /// @param _serviceRegistryTokenUtility ServiceRegistryTokenUtility contract address. - /// @param _securityToken Address of a service security token. + /// @param _stakingToken Address of a service security token. constructor( uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry, address _serviceRegistryTokenUtility, - address _securityToken + address _stakingToken ) ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) { // TODO: calculate minBalance // Initial checks - if (_securityToken == address(0) || _serviceRegistryTokenUtility == address(0)) { + if (_stakingToken == address(0) || _serviceRegistryTokenUtility == address(0)) { revert ZeroAddress(); } - securityToken = _securityToken; + stakingToken = _stakingToken; serviceRegistryTokenUtility = _serviceRegistryTokenUtility; } - /// @dev Safe token transferFrom implementation. - /// @notice The implementation is fully copied from the audited MIT-licensed solmate code repository: - /// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol - /// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract - /// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. - /// Also, the final `require` statement is modified with this contract own `revert` statement. - /// @param token Token address. - /// @param from Address to transfer tokens from. - /// @param to Address to transfer tokens to. - /// @param amount Token amount. - function safeTransferFrom(address token, address from, address to, uint256 amount) internal { - bool success; - - // solhint-disable-next-line no-inline-assembly - assembly { - // We'll write our calldata to this slot below, but restore it later. - let memPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000) - mstore(4, from) // Append the "from" argument. - mstore(36, to) // Append the "to" argument. - mstore(68, amount) // Append the "amount" argument. - - success := and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 100 because that's the total length of our calldata (4 + 32 * 3) - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - call(gas(), token, 0, 0, 100, 0, 32) - ) - - mstore(0x60, 0) // Restore the zero slot to zero. - mstore(0x40, memPointer) // Restore the memPointer. - } - - if (!success) { - revert TransferFailed(token, from, to, amount); - } - } - - /// @dev Safe token transfer implementation. - /// @notice The implementation is fully copied from the audited MIT-licensed solmate code repository: - /// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol - /// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract - /// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. - /// Also, the final `require` statement is modified with this contract own `revert` statement. - /// @param token Token address. - /// @param to Address to transfer tokens to. - /// @param amount Token amount. - function safeTransfer(address token, address to, uint256 amount) internal { - bool success; - - // solhint-disable-next-line no-inline-assembly - assembly { - // We'll write our calldata to this slot below, but restore it later. - let memPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(0, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) - mstore(4, to) // Append the "to" argument. - mstore(36, amount) // Append the "amount" argument. - - success := and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 68 because that's the total length of our calldata (4 + 32 * 2) - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - call(gas(), token, 0, 0, 68, 0, 32) - ) - - mstore(0x60, 0) // Restore the zero slot to zero. - mstore(0x40, memPointer) // Restore the memPointer. - } - - if (!success) { - revert TransferFailed(token, address(this), to, amount); - } - } - /// @dev Checks token security deposit. /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { @@ -156,7 +51,7 @@ contract ServiceStakingToken is ServiceStakingBase { IServiceTokenUtility(serviceRegistryTokenUtility).mapServiceIdTokenDeposit(serviceId); // The security token must match the contract token - if (securityToken != token) { + if (stakingToken != token) { revert(); } @@ -170,7 +65,10 @@ contract ServiceStakingToken is ServiceStakingBase { /// @param to Address to. /// @param amount Amount to withdraw. function _withdraw(address to, uint256 amount) internal override { - safeTransfer(securityToken, to, amount); + // Update the contract balance + balance -= amount; + + SafeTransferLib.safeTransfer(stakingToken, to, amount); emit Withdraw(to, amount); } @@ -182,18 +80,20 @@ contract ServiceStakingToken is ServiceStakingBase { _checkpoint(0); // Add to the overall balance - safeTransferFrom(securityToken, msg.sender, address(this), amount); + SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount); - // Add to the overall balance + // Add to the contract and available rewards balances uint256 newBalance = balance + amount; + uint256 newAvailableRewards = availableRewards + amount; // Update rewards per second - uint256 newRewardsPerSecond = (newBalance * apy) / (100 * 365 days); + uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); rewardsPerSecond = newRewardsPerSecond; - // Record the new actual balance + // Record the new actual balance and available rewards balance = newBalance; + availableRewards = newAvailableRewards; - emit Deposit(msg.sender, amount, newBalance, newRewardsPerSecond); + emit Deposit(msg.sender, amount, newBalance, newAvailableRewards, newRewardsPerSecond); } } \ No newline at end of file diff --git a/contracts/utils/SafeTransferLib.sol b/contracts/utils/SafeTransferLib.sol new file mode 100644 index 00000000..f0598189 --- /dev/null +++ b/contracts/utils/SafeTransferLib.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/// @dev Failure of a token transfer. +/// @param token Address of a token. +/// @param from Address `from`. +/// @param to Address `to`. +/// @param value Value. +error TokenTransferFailed(address token, address from, address to, uint256 value); + +/// @dev The implementation is fully copied from the audited MIT-licensed solmate code repository: +/// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol +/// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract +/// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. +/// Also, the final `require` statement is modified with this contract own `revert` statement. +library SafeTransferLib { + /// @dev Safe token transferFrom implementation. + /// @param token Token address. + /// @param from Address to transfer tokens from. + /// @param to Address to transfer tokens to. + /// @param amount Token amount. + function safeTransferFrom(address token, address from, address to, uint256 amount) internal { + bool success; + + // solhint-disable-next-line no-inline-assembly + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000) + mstore(4, from) // Append the "from" argument. + mstore(36, to) // Append the "to" argument. + mstore(68, amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 100 because that's the total length of our calldata (4 + 32 * 3) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0, 100, 0, 32) + ) + + mstore(0x60, 0) // Restore the zero slot to zero. + mstore(0x40, memPointer) // Restore the memPointer. + } + + if (!success) { + revert TokenTransferFailed(token, from, to, amount); + } + } + + /// @dev Safe token transfer implementation. + /// @notice The implementation is fully copied from the audited MIT-licensed solmate code repository: + /// https://github.com/transmissions11/solmate/blob/v7/src/utils/SafeTransferLib.sol + /// The original library imports the `ERC20` abstract token contract, and thus embeds all that contract + /// related code that is not needed. In this version, `ERC20` is swapped with the `address` representation. + /// Also, the final `require` statement is modified with this contract own `revert` statement. + /// @param token Token address. + /// @param to Address to transfer tokens to. + /// @param amount Token amount. + function safeTransfer(address token, address to, uint256 amount) internal { + bool success; + + // solhint-disable-next-line no-inline-assembly + assembly { + // We'll write our calldata to this slot below, but restore it later. + let memPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(0, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(4, to) // Append the "to" argument. + mstore(36, amount) // Append the "amount" argument. + + success := and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 68 because that's the total length of our calldata (4 + 32 * 2) + // Counterintuitively, this call() must be positioned after the or() in the + // surrounding and() because and() evaluates its arguments from right to left. + call(gas(), token, 0, 0, 68, 0, 32) + ) + + mstore(0x60, 0) // Restore the zero slot to zero. + mstore(0x40, memPointer) // Restore the memPointer. + } + + if (!success) { + revert TokenTransferFailed(token, address(this), to, amount); + } + } +} \ No newline at end of file From 5f44079aa8e623bb0623147d0e471f07d768aee3 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Mon, 25 Sep 2023 16:27:33 +0100 Subject: [PATCH 06/16] chore: change variable names --- contracts/staking/ServiceStaking.sol | 10 +++++----- contracts/staking/ServiceStakingBase.sol | 10 +++++----- contracts/staking/ServiceStakingToken.sol | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index 52882704..e4a0a5bf 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -11,11 +11,11 @@ import "../interfaces/IService.sol"; contract ServiceStaking is ServiceStakingBase { /// @dev ServiceStaking constructor. /// @param _apy Staking APY (in single digits). - /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @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). /// @param _serviceRegistry ServiceRegistry contract address. - constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) - ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) + constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) + ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) { // TODO: calculate minBalance } @@ -24,10 +24,10 @@ contract ServiceStaking is ServiceStakingBase { /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { // Get the service security token and deposit - (uint96 securityDeposit, , , , , , ) = IService(serviceRegistry).mapServices(serviceId); + (uint96 stakingDeposit, , , , , , ) = IService(serviceRegistry).mapServices(serviceId); // The security deposit must be greater or equal to the minimum defined one - if (securityDeposit < minSecurityDeposit) { + if (stakingDeposit < minStakingDeposit) { revert(); } } diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index ffa2cbb6..e8564146 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -34,7 +34,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { // APY value uint256 public immutable apy; // Minimum deposit value for staking - uint256 public immutable minSecurityDeposit; + uint256 public immutable minStakingDeposit; // Staking ratio in the format of 1e18 uint256 public immutable stakingRatio; // ServiceRegistry contract address @@ -57,12 +57,12 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @dev ServiceStakingBase constructor. /// @param _apy Staking APY (in single digits). - /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @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). /// @param _serviceRegistry ServiceRegistry contract address. - constructor(uint256 _apy, uint256 _minSecurityDeposit, uint256 _stakingRatio, address _serviceRegistry) { + constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) { // Initial checks - if (_apy == 0 || _minSecurityDeposit == 0 || _stakingRatio == 0) { + if (_apy == 0 || _minStakingDeposit == 0 || _stakingRatio == 0) { revert ZeroValue(); } if (_serviceRegistry == address(0)) { @@ -70,7 +70,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } apy = _apy; - minSecurityDeposit = _minSecurityDeposit; + minStakingDeposit = _minStakingDeposit; stakingRatio = _stakingRatio; serviceRegistry = _serviceRegistry; } diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index 0aa865c5..7d8f8df2 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -18,20 +18,20 @@ contract ServiceStakingToken is ServiceStakingBase { /// @dev ServiceStakingToken constructor. /// @param _apy Staking APY (in single digits). - /// @param _minSecurityDeposit Minimum security deposit for a service to be eligible to stake. + /// @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). /// @param _serviceRegistry ServiceRegistry contract address. /// @param _serviceRegistryTokenUtility ServiceRegistryTokenUtility contract address. /// @param _stakingToken Address of a service security token. constructor( uint256 _apy, - uint256 _minSecurityDeposit, + uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry, address _serviceRegistryTokenUtility, address _stakingToken ) - ServiceStakingBase(_apy, _minSecurityDeposit, _stakingRatio, _serviceRegistry) + ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) { // TODO: calculate minBalance // Initial checks @@ -47,7 +47,7 @@ contract ServiceStakingToken is ServiceStakingBase { /// @param serviceId Service Id. function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { // Get the service security token and deposit - (address token, uint96 securityDeposit) = + (address token, uint96 stakingDeposit) = IServiceTokenUtility(serviceRegistryTokenUtility).mapServiceIdTokenDeposit(serviceId); // The security token must match the contract token @@ -56,7 +56,7 @@ contract ServiceStakingToken is ServiceStakingBase { } // The security deposit must be greater or equal to the minimum defined one - if (securityDeposit < minSecurityDeposit) { + if (stakingDeposit < minStakingDeposit) { revert(); } } From d7801c9b8bdb26cb9e2684287f39551505e9a3cc Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Mon, 25 Sep 2023 23:25:40 +0100 Subject: [PATCH 07/16] refactor: more service stakng optimizations --- contracts/interfaces/IMultisig.sol | 4 -- contracts/interfaces/IService.sol | 27 +------- contracts/interfaces/IServiceTokenUtility.sol | 6 -- contracts/interfaces/IToken.sol | 4 ++ contracts/staking/ServiceStaking.sol | 17 +---- contracts/staking/ServiceStakingBase.sol | 68 ++++++++++++++++--- contracts/staking/ServiceStakingToken.sol | 55 ++++++++++++--- 7 files changed, 109 insertions(+), 72 deletions(-) diff --git a/contracts/interfaces/IMultisig.sol b/contracts/interfaces/IMultisig.sol index f09707fd..bc1034ff 100644 --- a/contracts/interfaces/IMultisig.sol +++ b/contracts/interfaces/IMultisig.sol @@ -13,8 +13,4 @@ interface IMultisig { uint256 threshold, bytes memory data ) external returns (address multisig); - - /// @dev Gets the multisig nonce. - /// @return Multisig nonce. - function nonce() external returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/IService.sol b/contracts/interfaces/IService.sol index 0d7e3a5a..08d16ce3 100644 --- a/contracts/interfaces/IService.sol +++ b/contracts/interfaces/IService.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.15; /// @dev Required interface for the service manipulation. -interface IService { +interface IService{ struct AgentParams { // Number of agent instances uint32 slots; @@ -87,29 +87,4 @@ interface IService { /// @return success True, if function executed successfully. /// @return refund The amount of refund returned to the operator. function unbond(address operator, uint256 serviceId) external returns (bool success, uint256 refund); - - /// @dev Transfers the service that was previously approved to this contract address. - /// @param from Account address to transfer from. - /// @param to Account address to transfer to. - /// @param id Service Id. - function transferFrom(address from, address to, uint256 id) external; - - /// @dev Gets the service instance from the map of services. - /// @param serviceId Service Id. - /// @return securityDeposit Registration activation deposit. - /// @return multisig Service multisig address. - /// @return configHash IPFS hashes pointing to the config metadata. - /// @return threshold Agent instance signers threshold. - /// @return maxNumAgentInstances Total number of agent instances. - /// @return numAgentInstances Actual number of agent instances. - /// @return state Service state. - function mapServices(uint256 serviceId) external view returns ( - uint96 securityDeposit, - address multisig, - bytes32 configHash, - uint32 threshold, - uint32 maxNumAgentInstances, - uint32 numAgentInstances, - uint8 state - ); } diff --git a/contracts/interfaces/IServiceTokenUtility.sol b/contracts/interfaces/IServiceTokenUtility.sol index e337bdba..d8fbdbaf 100644 --- a/contracts/interfaces/IServiceTokenUtility.sol +++ b/contracts/interfaces/IServiceTokenUtility.sol @@ -50,10 +50,4 @@ interface IServiceTokenUtility { /// @param serviceId Service Id. /// @return True if the service Id is token secured. function isTokenSecuredService(uint256 serviceId) external view returns (bool); - - /// @dev Gets the service security token info. - /// @param serviceId Service Id. - /// @return Token address. - /// @return Token security deposit. - function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); } diff --git a/contracts/interfaces/IToken.sol b/contracts/interfaces/IToken.sol index 813a8b08..66351aa5 100644 --- a/contracts/interfaces/IToken.sol +++ b/contracts/interfaces/IToken.sol @@ -41,4 +41,8 @@ interface IToken { /// @param amount Amount to transfer to. /// @return True if the function execution is successful. function transferFrom(address from, address to, uint256 amount) external returns (bool); + + /// @dev Gets the number of token decimals. + /// @return Number of token decimals. + function decimals() external view returns (uint8); } diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index e4a0a5bf..4d3a6a45 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.21; import {ServiceStakingBase} from "./ServiceStakingBase.sol"; -import "../interfaces/IService.sol"; /// @title ServiceStakingToken - Smart contract for staking a service by its owner when the service has an ETH as the deposit /// @author Aleksandr Kuperman - @@ -11,25 +10,13 @@ import "../interfaces/IService.sol"; contract ServiceStaking is ServiceStakingBase { /// @dev ServiceStaking constructor. /// @param _apy Staking APY (in single digits). - /// @param _minStakingDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) { - // TODO: calculate minBalance - } - - /// @dev Checks token security deposit. - /// @param serviceId Service Id. - function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { - // Get the service security token and deposit - (uint96 stakingDeposit, , , , , , ) = IService(serviceRegistry).mapServices(serviceId); - - // The security deposit must be greater or equal to the minimum defined one - if (stakingDeposit < minStakingDeposit) { - revert(); - } + minBalance = 1e14; } /// @dev Withdraws the reward amount to a service owner. diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index e8564146..259a1ef5 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -2,8 +2,46 @@ pragma solidity ^0.8.21; import "../interfaces/IErrorsRegistries.sol"; -import "../interfaces/IMultisig.sol"; -import "../interfaces/IService.sol"; + +// Multisig interface +interface IMultisig { + /// @dev Gets the multisig nonce. + /// @return Multisig nonce. + function nonce() external returns (uint256); +} + +// Service Registry interface +interface IService { + /// @dev Transfers the service that was previously approved to this contract address. + /// @param from Account address to transfer from. + /// @param to Account address to transfer to. + /// @param id Service Id. + function transferFrom(address from, address to, uint256 id) external; + + /// @dev Gets the service instance from the map of services. + /// @param serviceId Service Id. + /// @return securityDeposit Registration activation deposit. + /// @return multisig Service multisig address. + /// @return configHash IPFS hashes pointing to the config metadata. + /// @return threshold Agent instance signers threshold. + /// @return maxNumAgentInstances Total number of agent instances. + /// @return numAgentInstances Actual number of agent instances. + /// @return state Service state. + function mapServices(uint256 serviceId) external view returns ( + uint96 securityDeposit, + address multisig, + bytes32 configHash, + uint32 threshold, + uint32 maxNumAgentInstances, + uint32 numAgentInstances, + uint8 state + ); +} + +/// @dev Received lower value than the expected one. +/// @param provided Provided value is lower. +/// @param expected Expected value. +error LowerThan(uint256 provided, uint256 expected); // Service Info struct struct ServiceInfo { @@ -46,7 +84,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 public availableRewards; // Timestamp of the last checkpoint uint256 public tsCheckpoint; - // Minimum balance going below which would be given away, such that the contract balance is set to zero + // Minimum token / ETH balance, will be sent along with unstaked reward when going below that balance value uint256 public minBalance; // Rewards per second uint256 public rewardsPerSecond; @@ -57,7 +95,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @dev ServiceStakingBase constructor. /// @param _apy Staking APY (in single digits). - /// @param _minStakingDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) { @@ -75,9 +113,14 @@ abstract contract ServiceStakingBase is IErrorsRegistries { serviceRegistry = _serviceRegistry; } - /// @dev Checks token security deposit. - /// @param serviceId Service Id. - function _checkTokenSecurityDeposit(uint256 serviceId) internal view virtual {} + /// @dev Checks token / ETH staking deposit. + /// @param stakingDeposit Staking deposit. + function _checkTokenStakingDeposit(uint256, uint256 stakingDeposit) internal view virtual { + // The staking deposit derived from a security deposit value must be greater or equal to the minimum defined one + if (stakingDeposit < minStakingDeposit) { + revert LowerThan(stakingDeposit, minStakingDeposit); + } + } /// @dev Withdraws the reward amount to a service owner. /// @param to Address to. @@ -88,13 +131,13 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @param serviceId Service Id. function stake(uint256 serviceId) external { // Check the service conditions for staking - (, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); + (uint96 stakingDeposit, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); // The service must be deployed if (state != 4) { revert WrongServiceState(state, serviceId); } - // Check the service security deposit and token, if applicable - _checkTokenSecurityDeposit(serviceId); + // Check service staking deposit and token, if applicable + _checkTokenStakingDeposit(serviceId, stakingDeposit); // Transfer the service for staking IService(serviceRegistry).transferFrom(msg.sender, address(this), serviceId); @@ -207,6 +250,11 @@ abstract contract ServiceStakingBase is IErrorsRegistries { emit Checkpoint(lastAvailableRewards); } + /// @dev Public checkpoint function to allocate rewards up until a current time. + function checkpoint() external { + _checkpoint(0); + } + /// @dev Unstakes the service. /// @param serviceId Service Id. function unstake(uint256 serviceId) external { diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index 7d8f8df2..3c3efe27 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -4,13 +4,39 @@ pragma solidity ^0.8.21; import {ServiceStakingBase} from "./ServiceStakingBase.sol"; import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; import "../interfaces/IToken.sol"; -import "../interfaces/IServiceTokenUtility.sol"; + +// Service Registry Token Utility interface +interface IServiceTokenUtility { + /// @dev Gets the service security token info. + /// @param serviceId Service Id. + /// @return Token address. + /// @return Token security deposit. + function mapServiceIdTokenDeposit(uint256 serviceId) external view returns (address, uint96); +} + +/// @dev The token does not have enough decimals. +/// @param token Token address. +/// @param decimals Number of decimals. +error NotEnoughTokenDecimals(address token, uint8 decimals); + +/// @dev The staking token is wrong. +/// @param expected Expected staking token. +/// @param provided Provided staking token. +error WrongStakingToken(address expected, address provided); + +/// @dev Received lower value than the expected one. +/// @param provided Provided value is lower. +/// @param expected Expected value. +error LowerThan(uint256 provided, uint256 expected); /// @title ServiceStakingToken - Smart contract for staking a service by its owner when the service has an ERC20 token as the deposit /// @author Aleksandr Kuperman - /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - contract ServiceStakingToken is ServiceStakingBase { + // Minimum number of token decimals + uint8 public constant MIN_DECIMALS = 4; + // ServiceRegistryTokenUtility address address public immutable serviceRegistryTokenUtility; // Security token address for staking corresponding to the service deposit token @@ -18,11 +44,11 @@ contract ServiceStakingToken is ServiceStakingBase { /// @dev ServiceStakingToken constructor. /// @param _apy Staking APY (in single digits). - /// @param _minStakingDeposit Minimum security deposit for a service to be eligible to stake. + /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. /// @param _serviceRegistryTokenUtility ServiceRegistryTokenUtility contract address. - /// @param _stakingToken Address of a service security token. + /// @param _stakingToken Address of a service staking token. constructor( uint256 _apy, uint256 _minStakingDeposit, @@ -33,7 +59,6 @@ contract ServiceStakingToken is ServiceStakingBase { ) ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) { - // TODO: calculate minBalance // Initial checks if (_stakingToken == address(0) || _serviceRegistryTokenUtility == address(0)) { revert ZeroAddress(); @@ -41,23 +66,31 @@ contract ServiceStakingToken is ServiceStakingBase { stakingToken = _stakingToken; serviceRegistryTokenUtility = _serviceRegistryTokenUtility; + + // Calculate minBalance based on decimals + uint8 decimals = IToken(_stakingToken).decimals(); + if (decimals < MIN_DECIMALS) { + revert NotEnoughTokenDecimals(_stakingToken, decimals); + } else { + minBalance = 10 ** (decimals - MIN_DECIMALS); + } } - /// @dev Checks token security deposit. + /// @dev Checks token staking deposit. /// @param serviceId Service Id. - function _checkTokenSecurityDeposit(uint256 serviceId) internal view override { - // Get the service security token and deposit + function _checkTokenStakingDeposit(uint256 serviceId, uint256) internal view override { + // Get the service staking token and deposit (address token, uint96 stakingDeposit) = IServiceTokenUtility(serviceRegistryTokenUtility).mapServiceIdTokenDeposit(serviceId); - // The security token must match the contract token + // The staking token must match the contract token if (stakingToken != token) { - revert(); + revert WrongStakingToken(stakingToken, token); } - // The security deposit must be greater or equal to the minimum defined one + // The staking deposit must be greater or equal to the minimum defined one if (stakingDeposit < minStakingDeposit) { - revert(); + revert LowerThan(stakingDeposit, minStakingDeposit); } } From 9154cb9b7efda66b163dd15960cf56c444ae62fd Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 17:14:43 +0100 Subject: [PATCH 08/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStaking.sol | 9 +- contracts/staking/ServiceStakingBase.sol | 121 +++++++++++----------- contracts/staking/ServiceStakingToken.sol | 9 +- 3 files changed, 64 insertions(+), 75 deletions(-) diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index 4d3a6a45..aeaa48b9 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -34,21 +34,14 @@ contract ServiceStaking is ServiceStakingBase { } receive() external payable { - // Distribute current staking rewards - _checkpoint(0); - // Add to the contract and available rewards balances uint256 newBalance = balance + msg.value; uint256 newAvailableRewards = availableRewards + msg.value; - // Update rewards per second - uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); - rewardsPerSecond = newRewardsPerSecond; - // Record the new actual balance and available rewards balance = newBalance; availableRewards = newAvailableRewards; - emit Deposit(msg.sender, msg.value, newBalance, newAvailableRewards, newRewardsPerSecond); + emit Deposit(msg.sender, msg.value, newBalance, newAvailableRewards); } } \ No newline at end of file diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 259a1ef5..340ef28d 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -63,18 +63,17 @@ struct ServiceInfo { /// @author Mariapia Moscatiello - abstract contract ServiceStakingBase is IErrorsRegistries { event ServiceStaked(uint256 indexed serviceId, address indexed owner); - event Checkpoint(uint256 indexed balance); + event Checkpoint(uint256 indexed balance, uint256 numServices); event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward); - event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 newAvailableRewards, - uint256 rewardsPerSecond); + event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 newAvailableRewards); event Withdraw(address indexed to, uint256 amount); - // APY value - uint256 public immutable apy; + // Rewards per second + uint256 public immutable rewardsPerSecond; // Minimum deposit value for staking uint256 public immutable minStakingDeposit; - // Staking ratio in the format of 1e18 - uint256 public immutable stakingRatio; + // Liveness ratio in the format of 1e18 + uint256 public immutable livenessRatio; // ServiceRegistry contract address address public immutable serviceRegistry; @@ -86,30 +85,28 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 public tsCheckpoint; // Minimum token / ETH balance, will be sent along with unstaked reward when going below that balance value uint256 public minBalance; - // Rewards per second - uint256 public rewardsPerSecond; // Mapping of serviceId => staking service info mapping (uint256 => ServiceInfo) public mapServiceInfo; // Set of currently staking serviceIds uint256[] public setServiceIds; /// @dev ServiceStakingBase constructor. - /// @param _apy Staking APY (in single digits). + /// @param _rewardsPerSecond Staking rewards per second (in single digits). /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. - /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _livenessRatio Staking ratio: number of seconds per nonce (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. - constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) { + constructor(uint256 _rewardsPerSecond, uint256 _minStakingDeposit, uint256 _livenessRatio, address _serviceRegistry) { // Initial checks - if (_apy == 0 || _minStakingDeposit == 0 || _stakingRatio == 0) { + if (_rewardsPerSecond == 0 || _minStakingDeposit == 0 || _livenessRatio == 0) { revert ZeroValue(); } if (_serviceRegistry == address(0)) { revert ZeroAddress(); } - apy = _apy; + rewardsPerSecond = _rewardsPerSecond; minStakingDeposit = _minStakingDeposit; - stakingRatio = _stakingRatio; + livenessRatio = _livenessRatio; serviceRegistry = _serviceRegistry; } @@ -163,19 +160,25 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 lastAvailableRewards = availableRewards; uint256 tsCheckpointLast = tsCheckpoint; + // Number of services eligible for the reward during the current checkpoint + uint256 numServices; + // If available rewards are not zero, proceed with staking calculation // Otherwise, just bump the timestamp of last checkpoint - if (lastAvailableRewards > 0) { - uint256 numServices; + if (serviceId > 0 && lastAvailableRewards > 0) { uint256[] memory eligibleServiceIds = new uint256[](size); + uint256[] memory eligibleServiceRewards = new uint256[](size); + uint256[] memory serviceIds = new uint256[](size); + uint256[] memory serviceNonces = new uint256[](size); + uint256 totalRewards; // Calculate each staked service reward eligibility for (uint256 i = 0; i < size; ++i) { // Get the current service Id - uint256 curServiceId = setServiceIds[i]; + serviceIds[i] = setServiceIds[i]; // Get the service info - ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; + ServiceInfo storage curInfo = mapServiceInfo[serviceIds[i]]; // Calculate the staking nonce ratio uint256 curNonce = IMultisig(curInfo.multisig).nonce(); @@ -185,69 +188,69 @@ abstract contract ServiceStakingBase is IErrorsRegistries { if (curInfo.tsStart > tsCheckpointLast) { serviceCheckpoint = curInfo.tsStart; } - // Calculate the nonce ratio in 1e18 value - uint256 ratio = ((block.timestamp - serviceCheckpoint) * 1e18) / (curNonce - curInfo.nonce); + // Calculate the liveness ratio in 1e18 value + uint256 ratio; + // If the checkpoint was called in the exactly same block, the ratio is zero + if (block.timestamp > tsCheckpointLast) { + ratio = ((curNonce - curInfo.nonce) * 1e18) / (block.timestamp - serviceCheckpoint); + } // Record the reward for the service if it has provided enough transactions - if (ratio >= stakingRatio) { - eligibleServiceIds[numServices] = curServiceId; + if (ratio >= livenessRatio) { + // Calculate the reward up until now and record its value for the corresponding service + uint256 reward = rewardsPerSecond * (block.timestamp - serviceCheckpoint); + totalRewards += reward; + eligibleServiceRewards[numServices] = reward; + eligibleServiceIds[numServices] = serviceIds[i]; ++numServices; } - + // Record current service multisig nonce - curInfo.nonce = curNonce; + serviceNonces[i] = curNonce; // Record the unstaked service Id index in the global set of staked service Ids - if (curServiceId == serviceId) { + if (serviceIds[i] == serviceId) { idx = i; } } - // 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; - // Traverse all the eligible services and calculate their rewards - for (uint256 i = 0; i < numServices; ++i) { - uint256 curServiceId = eligibleServiceIds[i]; - ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; - - // Calculate the reward up until now - // Get the last service checkpoint: staking start time or the global checkpoint timestamp - uint256 serviceCheckpoint = tsCheckpointLast; - // Adjust the service checkpoint time if the service was staking less than the current staking period - if (curInfo.tsStart > tsCheckpointLast) { - serviceCheckpoint = curInfo.tsStart; - } - - // If the staking was longer than the deposited period, the service's timestamp is adjusted such that - // it is equal to at most the tsCheckpoint of the last deposit happening during every _checkpoint() call - uint256 reward = rewardsPerSecond * (block.timestamp - serviceCheckpoint); - // Adjust the reward if it goes out of calculated max bounds - if (reward > maxRewardsPerService) { - reward = maxRewardsPerService; + // 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 + for (uint256 i = 0; i < numServices; ++i) { + uint256 curServiceId = eligibleServiceIds[i]; + mapServiceInfo[curServiceId].reward += + (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards; } - // Adjust the available rewards value - if (lastAvailableRewards >= reward) { - lastAvailableRewards -= reward; - } else { - // This situation must never happen - // TODO: Fuzz this - reward = lastAvailableRewards; - lastAvailableRewards = 0; + // Set available rewards to zero + lastAvailableRewards = 0; + } else { + // Traverse all the eligible services and add to their rewards + for (uint256 i = 0; i < numServices; ++i) { + uint256 curServiceId = eligibleServiceIds[i]; + mapServiceInfo[curServiceId].reward += eligibleServiceRewards[i]; } - // Add the calculated reward to the service info - curInfo.reward += reward; + // Adjust available rewards + lastAvailableRewards -= totalRewards; } + // Update the storage value of available rewards availableRewards = lastAvailableRewards; + + // Updated current service nonces + for (uint256 i = 0; i < size; ++i) { + // Get the current service Id + uint256 curServiceId = serviceIds[i]; + mapServiceInfo[curServiceId].nonce = serviceNonces[i]; + } } // Record the current timestamp such that next calculations start from this point of time tsCheckpoint = block.timestamp; - emit Checkpoint(lastAvailableRewards); + emit Checkpoint(lastAvailableRewards, numServices); } /// @dev Public checkpoint function to allocate rewards up until a current time. diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index 3c3efe27..fa466757 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -109,9 +109,6 @@ contract ServiceStakingToken is ServiceStakingBase { /// @dev Deposits funds for staking. /// @param amount Token amount to deposit. function deposit(uint256 amount) external { - // Distribute current staking rewards - _checkpoint(0); - // Add to the overall balance SafeTransferLib.safeTransferFrom(stakingToken, msg.sender, address(this), amount); @@ -119,14 +116,10 @@ contract ServiceStakingToken is ServiceStakingBase { uint256 newBalance = balance + amount; uint256 newAvailableRewards = availableRewards + amount; - // Update rewards per second - uint256 newRewardsPerSecond = (newAvailableRewards * apy) / (100 * 365 days); - rewardsPerSecond = newRewardsPerSecond; - // Record the new actual balance and available rewards balance = newBalance; availableRewards = newAvailableRewards; - emit Deposit(msg.sender, amount, newBalance, newAvailableRewards, newRewardsPerSecond); + emit Deposit(msg.sender, amount, newBalance, newAvailableRewards); } } \ No newline at end of file From a013cc59c71180b7801633a0c010be9066903667 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 17:24:04 +0100 Subject: [PATCH 09/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 340ef28d..737fddc6 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -217,17 +217,27 @@ abstract contract ServiceStakingBase is IErrorsRegistries { // 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 + totalRewards = 0; for (uint256 i = 0; i < numServices; ++i) { + // Calculate the updated reward + uint256 updatedReward = (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards; + // Add to the total updated reward + totalRewards += updatedReward; + // Add reward to the service overall reward uint256 curServiceId = eligibleServiceIds[i]; - mapServiceInfo[curServiceId].reward += - (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards; + mapServiceInfo[curServiceId].reward += updatedReward; } + // If the reward adjustment happened to have small leftovers, add it to the last traversed service + if (lastAvailableRewards > totalRewards) { + mapServiceInfo[numServices - 1].reward += lastAvailableRewards - totalRewards; + } // Set available rewards to zero lastAvailableRewards = 0; } else { // Traverse all the eligible services and add to their rewards for (uint256 i = 0; i < numServices; ++i) { + // Add reward to the service overall reward uint256 curServiceId = eligibleServiceIds[i]; mapServiceInfo[curServiceId].reward += eligibleServiceRewards[i]; } From c61db332de67e53b5ca11bc0593f130574fcc859 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 22:54:43 +0100 Subject: [PATCH 10/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 144 +++++++++++++++++------ 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 737fddc6..d115a355 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -7,7 +7,7 @@ import "../interfaces/IErrorsRegistries.sol"; interface IMultisig { /// @dev Gets the multisig nonce. /// @return Multisig nonce. - function nonce() external returns (uint256); + function nonce() external view returns (uint256); } // Service Registry interface @@ -43,6 +43,10 @@ interface IService { /// @param expected Expected value. error LowerThan(uint256 provided, uint256 expected); +/// @dev Service is not staked. +/// @param serviceId Service Id. +error ServiceNotStaked(uint256 serviceId); + // Service Info struct struct ServiceInfo { // Service multisig address @@ -152,36 +156,56 @@ abstract contract ServiceStakingBase is IErrorsRegistries { emit ServiceStaked(serviceId, msg.sender); } - /// @dev Checkpoint to allocate rewards up until a current time. - /// @param serviceId Service Id that unstakes, or 0 if the function is called during the deposit of new funds. - function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { - // Get the service Id set length - uint256 size = setServiceIds.length; - uint256 lastAvailableRewards = availableRewards; + /// @dev Calculates staking rewards for all services at current timestamp. + /// @param lastAvailableRewards Available amount of rewards. + /// @param numServices Number of services eligible for the reward that passed the liveness check. + /// @param totalRewards Total calculated rewards. + /// @param eligibleServiceIds Service Ids eligible for rewards. + /// @param eligibleServiceRewards Corresponding rewards for eligible service Ids. + /// @param serviceIds All the staking service Ids. + /// @param serviceNonces Current service nonces. + function _calculateStakingRewards() internal view returns ( + uint256 lastAvailableRewards, + uint256 numServices, + uint256 totalRewards, + uint256[] memory eligibleServiceIds, + uint256[] memory eligibleServiceRewards, + uint256[] memory serviceIds, + uint256[] memory serviceNonces + ) + { + // Get available rewards and last checkpoint timestamp + lastAvailableRewards = availableRewards; uint256 tsCheckpointLast = tsCheckpoint; - // Number of services eligible for the reward during the current checkpoint - uint256 numServices; + // Get the service Ids set length + uint256 size = setServiceIds.length; + serviceIds = new uint256[](size); + serviceNonces = new uint256[](size); + + // Record service Ids and nonces + for (uint256 i = 0; i < size; ++i) { + // Get current service Id + serviceIds[i] = setServiceIds[i]; + + // Get current service multisig nonce + address multisig = mapServiceInfo[serviceIds[i]].multisig; + serviceNonces[i] = IMultisig(multisig).nonce(); + } // If available rewards are not zero, proceed with staking calculation - // Otherwise, just bump the timestamp of last checkpoint - if (serviceId > 0 && lastAvailableRewards > 0) { - uint256[] memory eligibleServiceIds = new uint256[](size); - uint256[] memory eligibleServiceRewards = new uint256[](size); - uint256[] memory serviceIds = new uint256[](size); - uint256[] memory serviceNonces = new uint256[](size); - uint256 totalRewards; + if (lastAvailableRewards > 0) { + // Get necessary arrays + eligibleServiceIds = new uint256[](size); + eligibleServiceRewards = new uint256[](size); // Calculate each staked service reward eligibility for (uint256 i = 0; i < size; ++i) { - // Get the current service Id - serviceIds[i] = setServiceIds[i]; - // Get the service info - ServiceInfo storage curInfo = mapServiceInfo[serviceIds[i]]; + uint256 curServiceId = serviceIds[i]; + ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; // Calculate the staking nonce ratio - uint256 curNonce = IMultisig(curInfo.multisig).nonce(); // Get the last service checkpoint: staking start time or the global checkpoint timestamp uint256 serviceCheckpoint = tsCheckpointLast; // Adjust the service checkpoint time if the service was staking less than the current staking period @@ -192,7 +216,8 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 ratio; // If the checkpoint was called in the exactly same block, the ratio is zero if (block.timestamp > tsCheckpointLast) { - ratio = ((curNonce - curInfo.nonce) * 1e18) / (block.timestamp - serviceCheckpoint); + uint256 nonce = serviceNonces[i]; + ratio = ((nonce - curInfo.nonce) * 1e18) / (block.timestamp - serviceCheckpoint); } // Record the reward for the service if it has provided enough transactions @@ -201,19 +226,24 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 reward = rewardsPerSecond * (block.timestamp - serviceCheckpoint); totalRewards += reward; eligibleServiceRewards[numServices] = reward; - eligibleServiceIds[numServices] = serviceIds[i]; + eligibleServiceIds[numServices] = curServiceId; ++numServices; } - - // Record current service multisig nonce - serviceNonces[i] = curNonce; - - // Record the unstaked service Id index in the global set of staked service Ids - if (serviceIds[i] == serviceId) { - idx = i; - } } + } + } + /// @dev Checkpoint to allocate rewards up until a current time. + /// @param serviceId Service Id that is being unstaked, or 0 if the checkpoint is called by itself. + /// @return idx Index of a service Id in a global set of service Ids. + function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { + // Calculate staking rewards + (uint256 lastAvailableRewards, uint256 numServices, uint256 totalRewards, + uint256[] memory eligibleServiceIds, uint256[] memory eligibleServiceRewards, + uint256[] memory serviceIds, uint256[] memory serviceNonces) = _calculateStakingRewards(); + + // If available rewards are not zero, proceed with staking calculation + if (lastAvailableRewards > 0) { // 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 @@ -248,12 +278,17 @@ abstract contract ServiceStakingBase is IErrorsRegistries { // Update the storage value of available rewards availableRewards = lastAvailableRewards; + } - // Updated current service nonces - for (uint256 i = 0; i < size; ++i) { - // Get the current service Id - uint256 curServiceId = serviceIds[i]; - mapServiceInfo[curServiceId].nonce = serviceNonces[i]; + // Updated current service nonces and get the index of the unstaked service Id + for (uint256 i = 0; i < serviceIds.length; ++i) { + // Get the current service Id + uint256 curServiceId = serviceIds[i]; + mapServiceInfo[curServiceId].nonce = serviceNonces[i]; + + // If applicable, record the unstaked service Id index + if (curServiceId == serviceId) { + idx = i; } } @@ -278,6 +313,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } // Call the checkpoint and 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 = _checkpoint(serviceId); // Transfer the service back to the owner @@ -306,4 +342,40 @@ abstract contract ServiceStakingBase is IErrorsRegistries { emit ServiceUnstaked(serviceId, msg.sender, amount); } + + /// @dev Calculates service staking reward at current timestamp. + /// @param serviceId Service Id. + /// @return reward Service reward. + function calculateServiceStakingReward(uint256 serviceId) external view returns (uint256 reward) { + ServiceInfo memory sInfo = mapServiceInfo[serviceId]; + + // Check if the service is staked + if (sInfo.tsStart == 0) { + revert ServiceNotStaked(serviceId); + } + + // Calculate overall staking rewards + (uint256 lastAvailableRewards, , uint256 totalRewards, , uint256[] memory eligibleServiceRewards, + uint256[] memory serviceIds, ) = _calculateStakingRewards(); + + + // If available rewards are not zero, proceed with staking calculation + if (lastAvailableRewards > 0) { + // Get the service index + uint256 idx; + for (uint256 i = 0; i < serviceIds.length; ++i) { + if (serviceIds[i] == serviceId) { + idx = i; + break; + } + } + + // If total allocated rewards are not enough, adjust the reward value + if (totalRewards > lastAvailableRewards) { + reward = sInfo.reward + (eligibleServiceRewards[idx] * lastAvailableRewards) / totalRewards; + } else { + reward = sInfo.reward + eligibleServiceRewards[idx]; + } + } + } } \ No newline at end of file From 1d13365094da398ac562605800795d9d9c41fe75 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 23:39:25 +0100 Subject: [PATCH 11/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 43 ++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index d115a355..ad665ca7 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -67,9 +67,9 @@ struct ServiceInfo { /// @author Mariapia Moscatiello - abstract contract ServiceStakingBase is IErrorsRegistries { event ServiceStaked(uint256 indexed serviceId, address indexed owner); - event Checkpoint(uint256 indexed balance, uint256 numServices); - event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward); - event Deposit(address indexed sender, uint256 amount, uint256 newBalance, uint256 newAvailableRewards); + event Checkpoint(uint256 availableRewards, uint256 numServices); + event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward, uint256 tsStart); + event Deposit(address indexed sender, uint256 amount, uint256 balance, uint256 availableRewards); event Withdraw(address indexed to, uint256 amount); // Rewards per second @@ -215,7 +215,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { // Calculate the liveness ratio in 1e18 value uint256 ratio; // If the checkpoint was called in the exactly same block, the ratio is zero - if (block.timestamp > tsCheckpointLast) { + if (block.timestamp > serviceCheckpoint) { uint256 nonce = serviceNonces[i]; ratio = ((nonce - curInfo.nonce) * 1e18) / (block.timestamp - serviceCheckpoint); } @@ -234,9 +234,8 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } /// @dev Checkpoint to allocate rewards up until a current time. - /// @param serviceId Service Id that is being unstaked, or 0 if the checkpoint is called by itself. - /// @return idx Index of a service Id in a global set of service Ids. - function _checkpoint(uint256 serviceId) internal returns (uint256 idx) { + /// @return allServiceIds All staking service Ids. + function checkpoint() public returns (uint256[] memory allServiceIds) { // Calculate staking rewards (uint256 lastAvailableRewards, uint256 numServices, uint256 totalRewards, uint256[] memory eligibleServiceIds, uint256[] memory eligibleServiceRewards, @@ -280,16 +279,12 @@ abstract contract ServiceStakingBase is IErrorsRegistries { availableRewards = lastAvailableRewards; } - // Updated current service nonces and get the index of the unstaked service Id + // Updated current service nonces and get service Ids for (uint256 i = 0; i < serviceIds.length; ++i) { // Get the current service Id uint256 curServiceId = serviceIds[i]; + allServiceIds[i] = serviceIds[i]; mapServiceInfo[curServiceId].nonce = serviceNonces[i]; - - // If applicable, record the unstaked service Id index - if (curServiceId == serviceId) { - idx = i; - } } // Record the current timestamp such that next calculations start from this point of time @@ -298,11 +293,6 @@ abstract contract ServiceStakingBase is IErrorsRegistries { emit Checkpoint(lastAvailableRewards, numServices); } - /// @dev Public checkpoint function to allocate rewards up until a current time. - function checkpoint() external { - _checkpoint(0); - } - /// @dev Unstakes the service. /// @param serviceId Service Id. function unstake(uint256 serviceId) external { @@ -312,9 +302,17 @@ abstract contract ServiceStakingBase is IErrorsRegistries { revert OwnerOnly(msg.sender, sInfo.owner); } - // Call the checkpoint and get the service index in the set of services + // Call the checkpoint + uint256[] memory serviceIds = checkpoint(); + + // 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 = _checkpoint(serviceId); + uint256 idx; + for (; idx < serviceIds.length; ++idx) { + if (serviceIds[idx] == serviceId) { + break; + } + } // Transfer the service back to the owner IService(serviceRegistry).transferFrom(address(this), msg.sender, serviceId); @@ -332,6 +330,9 @@ abstract contract ServiceStakingBase is IErrorsRegistries { _withdraw(sInfo.multisig, amount); } + // Record staking start time + uint256 tsStart = sInfo.tsStart; + // Clear all the data about the unstaked service // Delete the service info struct delete mapServiceInfo[serviceId]; @@ -340,7 +341,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; setServiceIds.pop(); - emit ServiceUnstaked(serviceId, msg.sender, amount); + emit ServiceUnstaked(serviceId, msg.sender, amount, tsStart); } /// @dev Calculates service staking reward at current timestamp. From e409d0c9bbd8077d9c4de79082b012cfbd5d3fc3 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 23:56:26 +0100 Subject: [PATCH 12/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index ad665ca7..6c156924 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -234,8 +234,8 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } /// @dev Checkpoint to allocate rewards up until a current time. - /// @return allServiceIds All staking service Ids. - function checkpoint() public returns (uint256[] memory allServiceIds) { + /// @return All staking service Ids. + function checkpoint() public returns (uint256[] memory) { // Calculate staking rewards (uint256 lastAvailableRewards, uint256 numServices, uint256 totalRewards, uint256[] memory eligibleServiceIds, uint256[] memory eligibleServiceRewards, @@ -291,6 +291,8 @@ abstract contract ServiceStakingBase is IErrorsRegistries { tsCheckpoint = block.timestamp; emit Checkpoint(lastAvailableRewards, numServices); + + return serviceIds; } /// @dev Unstakes the service. From 1ca785c22b248e9382a35df75b44f2c829d95b35 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Tue, 26 Sep 2023 23:56:54 +0100 Subject: [PATCH 13/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 6c156924..70fcccd2 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -279,11 +279,10 @@ abstract contract ServiceStakingBase is IErrorsRegistries { availableRewards = lastAvailableRewards; } - // Updated current service nonces and get service Ids + // Updated current service nonces for (uint256 i = 0; i < serviceIds.length; ++i) { // Get the current service Id uint256 curServiceId = serviceIds[i]; - allServiceIds[i] = serviceIds[i]; mapServiceInfo[curServiceId].nonce = serviceNonces[i]; } From 9d6e99084774543614a1e20357a080d2371def2e Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 27 Sep 2023 09:07:42 +0100 Subject: [PATCH 14/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 70fcccd2..daab3557 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -349,7 +349,9 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @param serviceId Service Id. /// @return reward Service reward. function calculateServiceStakingReward(uint256 serviceId) external view returns (uint256 reward) { + // Get current service reward ServiceInfo memory sInfo = mapServiceInfo[serviceId]; + reward = sInfo.reward; // Check if the service is staked if (sInfo.tsStart == 0) { @@ -357,27 +359,23 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } // Calculate overall staking rewards - (uint256 lastAvailableRewards, , uint256 totalRewards, , uint256[] memory eligibleServiceRewards, - uint256[] memory serviceIds, ) = _calculateStakingRewards(); - + (uint256 lastAvailableRewards, , uint256 totalRewards, uint256[] memory eligibleServiceIds, + uint256[] memory eligibleServiceRewards, , ) = _calculateStakingRewards(); // If available rewards are not zero, proceed with staking calculation if (lastAvailableRewards > 0) { - // Get the service index - uint256 idx; - for (uint256 i = 0; i < serviceIds.length; ++i) { - if (serviceIds[i] == serviceId) { - idx = i; + // Get the service index in the eligible service set and calculate its latest reward + for (uint256 i = 0; i < eligibleServiceIds.length; ++i) { + if (eligibleServiceIds[i] == serviceId) { + // If total allocated rewards are not enough, adjust the reward value + if (totalRewards > lastAvailableRewards) { + reward += (eligibleServiceRewards[i] * lastAvailableRewards) / totalRewards; + } else { + reward += eligibleServiceRewards[i]; + } break; } } - - // If total allocated rewards are not enough, adjust the reward value - if (totalRewards > lastAvailableRewards) { - reward = sInfo.reward + (eligibleServiceRewards[idx] * lastAvailableRewards) / totalRewards; - } else { - reward = sInfo.reward + eligibleServiceRewards[idx]; - } } } } \ No newline at end of file From 78da5105f8eca50f96a98f04caf33b52f6a2604d Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 27 Sep 2023 09:30:10 +0100 Subject: [PATCH 15/16] refactor: more code optimization for service staking --- contracts/staking/ServiceStakingBase.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index daab3557..65ed1f11 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -235,7 +235,10 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @dev Checkpoint to allocate rewards up until a current time. /// @return All staking service Ids. - function checkpoint() public returns (uint256[] memory) { + /// @return Number of staking eligible services. + /// @return Eligible service Ids. + /// @return Eligible service rewards. + function checkpoint() public returns (uint256[] memory, uint256, uint256[] memory, uint256[] memory) { // Calculate staking rewards (uint256 lastAvailableRewards, uint256 numServices, uint256 totalRewards, uint256[] memory eligibleServiceIds, uint256[] memory eligibleServiceRewards, @@ -291,7 +294,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { emit Checkpoint(lastAvailableRewards, numServices); - return serviceIds; + return (serviceIds, numServices, eligibleServiceIds, eligibleServiceRewards); } /// @dev Unstakes the service. @@ -304,7 +307,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { } // Call the checkpoint - uint256[] memory serviceIds = checkpoint(); + (uint256[] memory serviceIds, , , ) = checkpoint(); // 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 From 68af80423942af570455f4f8076c84af0b385d03 Mon Sep 17 00:00:00 2001 From: Aleksandr Kuperman Date: Wed, 27 Sep 2023 14:21:29 +0100 Subject: [PATCH 16/16] refactor: adding max number of staking services --- contracts/staking/ServiceStaking.sol | 19 ++++--- contracts/staking/ServiceStakingBase.sol | 68 ++++++++++++++--------- contracts/staking/ServiceStakingToken.sol | 23 +++----- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/contracts/staking/ServiceStaking.sol b/contracts/staking/ServiceStaking.sol index aeaa48b9..c1f73e7e 100644 --- a/contracts/staking/ServiceStaking.sol +++ b/contracts/staking/ServiceStaking.sol @@ -9,15 +9,20 @@ import {ServiceStakingBase} from "./ServiceStakingBase.sol"; /// @author Mariapia Moscatiello - contract ServiceStaking is ServiceStakingBase { /// @dev ServiceStaking constructor. - /// @param _apy Staking APY (in single digits). + /// @param _maxNumServices Maximum number of staking services. + /// @param _rewardsPerSecond Staking rewards per second (in single digits). /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. - /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _livenessRatio Liveness ratio: number of nonces per second (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. - constructor(uint256 _apy, uint256 _minStakingDeposit, uint256 _stakingRatio, address _serviceRegistry) - ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) - { - minBalance = 1e14; - } + constructor( + uint256 _maxNumServices, + uint256 _rewardsPerSecond, + uint256 _minStakingDeposit, + uint256 _livenessRatio, + address _serviceRegistry + ) + ServiceStakingBase(_maxNumServices, _rewardsPerSecond, _minStakingDeposit, _livenessRatio, _serviceRegistry) + {} /// @dev Withdraws the reward amount to a service owner. /// @param to Address to. diff --git a/contracts/staking/ServiceStakingBase.sol b/contracts/staking/ServiceStakingBase.sol index 65ed1f11..4fdcd6e4 100644 --- a/contracts/staking/ServiceStakingBase.sol +++ b/contracts/staking/ServiceStakingBase.sol @@ -38,6 +38,13 @@ interface IService { ); } +/// @dev No rewards are available in the contract. +error NoRewardsAvailable(); + +/// @dev Maximum number of staking services is reached. +/// @param maxNumServices Maximum number of staking services. +error MaxNumServicesReached(uint256 maxNumServices); + /// @dev Received lower value than the expected one. /// @param provided Provided value is lower. /// @param expected Expected value. @@ -66,15 +73,18 @@ struct ServiceInfo { /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - abstract contract ServiceStakingBase is IErrorsRegistries { - event ServiceStaked(uint256 indexed serviceId, address indexed owner); + event ServiceStaked(uint256 indexed serviceId, address indexed owner, address indexed multisig, uint256 nonce); event Checkpoint(uint256 availableRewards, uint256 numServices); - event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, uint256 reward, uint256 tsStart); + event ServiceUnstaked(uint256 indexed serviceId, address indexed owner, address indexed multisig, uint256 nonce, + uint256 reward, uint256 tsStart); event Deposit(address indexed sender, uint256 amount, uint256 balance, uint256 availableRewards); event Withdraw(address indexed to, uint256 amount); + // Maximum number of staking services + uint256 public immutable maxNumServices; // Rewards per second uint256 public immutable rewardsPerSecond; - // Minimum deposit value for staking + // Minimum service staking deposit value required for staking uint256 public immutable minStakingDeposit; // Liveness ratio in the format of 1e18 uint256 public immutable livenessRatio; @@ -87,21 +97,26 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 public availableRewards; // Timestamp of the last checkpoint uint256 public tsCheckpoint; - // Minimum token / ETH balance, will be sent along with unstaked reward when going below that balance value - uint256 public minBalance; // Mapping of serviceId => staking service info mapping (uint256 => ServiceInfo) public mapServiceInfo; // Set of currently staking serviceIds uint256[] public setServiceIds; /// @dev ServiceStakingBase constructor. + /// @param _maxNumServices Maximum number of staking services. /// @param _rewardsPerSecond Staking rewards per second (in single digits). /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. - /// @param _livenessRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _livenessRatio Liveness ratio: number of nonces per second (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. - constructor(uint256 _rewardsPerSecond, uint256 _minStakingDeposit, uint256 _livenessRatio, address _serviceRegistry) { + constructor( + uint256 _maxNumServices, + uint256 _rewardsPerSecond, + uint256 _minStakingDeposit, + uint256 _livenessRatio, + address _serviceRegistry) + { // Initial checks - if (_rewardsPerSecond == 0 || _minStakingDeposit == 0 || _livenessRatio == 0) { + if (_maxNumServices == 0 || _rewardsPerSecond == 0 || _minStakingDeposit == 0 || _livenessRatio == 0) { revert ZeroValue(); } if (_serviceRegistry == address(0)) { @@ -131,6 +146,17 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @dev Stakes the service. /// @param serviceId Service Id. function stake(uint256 serviceId) external { + // Check if there available rewards + if (availableRewards == 0) { + revert NoRewardsAvailable(); + } + + // Check for the maximum number of staking services + uint256 numStakingServices = setServiceIds.length; + if (numStakingServices == maxNumServices) { + revert MaxNumServicesReached(maxNumServices); + } + // Check the service conditions for staking (uint96 stakingDeposit, address multisig, , , , , uint8 state) = IService(serviceRegistry).mapServices(serviceId); // The service must be deployed @@ -147,13 +173,14 @@ abstract contract ServiceStakingBase is IErrorsRegistries { ServiceInfo storage sInfo = mapServiceInfo[serviceId]; sInfo.multisig = multisig; sInfo.owner = msg.sender; - sInfo.nonce = IMultisig(multisig).nonce(); + uint256 nonce = IMultisig(multisig).nonce(); + sInfo.nonce = nonce; sInfo.tsStart = block.timestamp; // Add the service Id to the set of staked services setServiceIds.push(serviceId); - emit ServiceStaked(serviceId, msg.sender); + emit ServiceStaked(serviceId, msg.sender, multisig, nonce); } /// @dev Calculates staking rewards for all services at current timestamp. @@ -205,7 +232,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { uint256 curServiceId = serviceIds[i]; ServiceInfo storage curInfo = mapServiceInfo[curServiceId]; - // Calculate the staking nonce ratio + // Calculate the liveness nonce ratio // Get the last service checkpoint: staking start time or the global checkpoint timestamp uint256 serviceCheckpoint = tsCheckpointLast; // Adjust the service checkpoint time if the service was staking less than the current staking period @@ -300,7 +327,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { /// @dev Unstakes the service. /// @param serviceId Service Id. function unstake(uint256 serviceId) external { - ServiceInfo storage sInfo = mapServiceInfo[serviceId]; + ServiceInfo memory sInfo = mapServiceInfo[serviceId]; // Check for the service ownership if (msg.sender != sInfo.owner) { revert OwnerOnly(msg.sender, sInfo.owner); @@ -321,22 +348,11 @@ abstract contract ServiceStakingBase is IErrorsRegistries { // Transfer the service back to the owner IService(serviceRegistry).transferFrom(address(this), msg.sender, serviceId); - // Send the remaining small balance along with the reward if it is below the chosen threshold - uint256 amount = sInfo.reward; - uint256 lastAvailableRewards = availableRewards; - if (lastAvailableRewards < minBalance) { - amount += lastAvailableRewards; - availableRewards = 0; - } - // Transfer accumulated rewards to the service multisig - if (amount > 0) { - _withdraw(sInfo.multisig, amount); + if (sInfo.reward > 0) { + _withdraw(sInfo.multisig, sInfo.reward); } - // Record staking start time - uint256 tsStart = sInfo.tsStart; - // Clear all the data about the unstaked service // Delete the service info struct delete mapServiceInfo[serviceId]; @@ -345,7 +361,7 @@ abstract contract ServiceStakingBase is IErrorsRegistries { setServiceIds[idx] = setServiceIds[setServiceIds.length - 1]; setServiceIds.pop(); - emit ServiceUnstaked(serviceId, msg.sender, amount, tsStart); + emit ServiceUnstaked(serviceId, msg.sender, sInfo.multisig, sInfo.nonce, sInfo.reward, sInfo.tsStart); } /// @dev Calculates service staking reward at current timestamp. diff --git a/contracts/staking/ServiceStakingToken.sol b/contracts/staking/ServiceStakingToken.sol index fa466757..50c4a7cc 100644 --- a/contracts/staking/ServiceStakingToken.sol +++ b/contracts/staking/ServiceStakingToken.sol @@ -34,30 +34,29 @@ error LowerThan(uint256 provided, uint256 expected); /// @author Andrey Lebedev - /// @author Mariapia Moscatiello - contract ServiceStakingToken is ServiceStakingBase { - // Minimum number of token decimals - uint8 public constant MIN_DECIMALS = 4; - // ServiceRegistryTokenUtility address address public immutable serviceRegistryTokenUtility; // Security token address for staking corresponding to the service deposit token address public immutable stakingToken; /// @dev ServiceStakingToken constructor. - /// @param _apy Staking APY (in single digits). + /// @param _maxNumServices Maximum number of staking services. + /// @param _rewardsPerSecond Staking rewards per second (in single digits). /// @param _minStakingDeposit Minimum staking deposit for a service to be eligible to stake. - /// @param _stakingRatio Staking ratio: number of seconds per nonce (in 18 digits). + /// @param _livenessRatio Liveness ratio: number of nonces per second (in 18 digits). /// @param _serviceRegistry ServiceRegistry contract address. /// @param _serviceRegistryTokenUtility ServiceRegistryTokenUtility contract address. /// @param _stakingToken Address of a service staking token. constructor( - uint256 _apy, + uint256 _maxNumServices, + uint256 _rewardsPerSecond, uint256 _minStakingDeposit, - uint256 _stakingRatio, + uint256 _livenessRatio, address _serviceRegistry, address _serviceRegistryTokenUtility, address _stakingToken ) - ServiceStakingBase(_apy, _minStakingDeposit, _stakingRatio, _serviceRegistry) + ServiceStakingBase(_maxNumServices, _rewardsPerSecond, _minStakingDeposit, _livenessRatio, _serviceRegistry) { // Initial checks if (_stakingToken == address(0) || _serviceRegistryTokenUtility == address(0)) { @@ -66,14 +65,6 @@ contract ServiceStakingToken is ServiceStakingBase { stakingToken = _stakingToken; serviceRegistryTokenUtility = _serviceRegistryTokenUtility; - - // Calculate minBalance based on decimals - uint8 decimals = IToken(_stakingToken).decimals(); - if (decimals < MIN_DECIMALS) { - revert NotEnoughTokenDecimals(_stakingToken, decimals); - } else { - minBalance = 10 ** (decimals - MIN_DECIMALS); - } } /// @dev Checks token staking deposit.