From 122b31834b56346725e1457028892d1b338d259e Mon Sep 17 00:00:00 2001 From: Uladzislau Hubar Date: Wed, 24 Jan 2024 18:08:53 +0100 Subject: [PATCH] Added custom errors for Staking contract, exposed addStake function for delegators, added usage of ShardingTableV2 for StakingV2 --- contracts/v1/ServiceAgreementV1.sol | 9 +- contracts/v1/errors/GeneralErrors.sol | 1 + .../v1/errors/ServiceAgreementErrorsV1.sol | 2 - .../v1/errors/ServiceAgreementErrorsV1U1.sol | 2 - contracts/v1/errors/TokenErrors.sol | 8 + contracts/v1/storage/ShardingTableStorage.sol | 14 +- contracts/v2/Staking.sol | 269 ++++++++++++++++++ contracts/v2/errors/ProfileErrors.sol | 7 + contracts/v2/errors/StakingErrors.sol | 11 + 9 files changed, 308 insertions(+), 15 deletions(-) create mode 100644 contracts/v1/errors/TokenErrors.sol create mode 100644 contracts/v2/Staking.sol create mode 100644 contracts/v2/errors/ProfileErrors.sol create mode 100644 contracts/v2/errors/StakingErrors.sol diff --git a/contracts/v1/ServiceAgreementV1.sol b/contracts/v1/ServiceAgreementV1.sol index bf71bd4b..71582bab 100644 --- a/contracts/v1/ServiceAgreementV1.sol +++ b/contracts/v1/ServiceAgreementV1.sol @@ -16,6 +16,7 @@ import {Named} from "./interface/Named.sol"; import {Versioned} from "./interface/Versioned.sol"; import {ServiceAgreementStructsV1} from "./structs/ServiceAgreementStructsV1.sol"; import {GeneralErrors} from "./errors/GeneralErrors.sol"; +import {TokenErrors} from "./errors/TokenErrors.sol"; import {ServiceAgreementErrorsV1U1} from "./errors/ServiceAgreementErrorsV1U1.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -106,9 +107,9 @@ contract ServiceAgreementV1 is Named, Versioned, ContractStatus, Initializable { IERC20 tknc = tokenContract; if (tknc.allowance(args.assetCreator, address(this)) < args.tokenAmount) - revert ServiceAgreementErrorsV1U1.TooLowAllowance(tknc.allowance(args.assetCreator, address(this))); + revert TokenErrors.TooLowAllowance(address(tknc), tknc.allowance(args.assetCreator, address(this))); if (tknc.balanceOf(args.assetCreator) < args.tokenAmount) - revert ServiceAgreementErrorsV1U1.TooLowBalance(tknc.balanceOf(args.assetCreator)); + revert TokenErrors.TooLowBalance(address(tknc), tknc.balanceOf(args.assetCreator)); tknc.transferFrom(args.assetCreator, sasProxy.agreementV1StorageAddress(), args.tokenAmount); @@ -253,9 +254,9 @@ contract ServiceAgreementV1 is Named, Versioned, ContractStatus, Initializable { IERC20 tknc = tokenContract; if (tknc.allowance(assetOwner, address(this)) < tokenAmount) - revert ServiceAgreementErrorsV1U1.TooLowAllowance(tknc.allowance(assetOwner, address(this))); + revert TokenErrors.TooLowAllowance(address(tknc), tknc.allowance(assetOwner, address(this))); if (tknc.balanceOf(assetOwner) < tokenAmount) - revert ServiceAgreementErrorsV1U1.TooLowBalance(tknc.balanceOf(assetOwner)); + revert TokenErrors.TooLowBalance(address(tknc), tknc.balanceOf(assetOwner)); tknc.transferFrom(assetOwner, sasAddress, tokenAmount); } diff --git a/contracts/v1/errors/GeneralErrors.sol b/contracts/v1/errors/GeneralErrors.sol index ef0b8ec1..a3aae4f0 100644 --- a/contracts/v1/errors/GeneralErrors.sol +++ b/contracts/v1/errors/GeneralErrors.sol @@ -5,4 +5,5 @@ pragma solidity ^0.8.16; library GeneralErrors { error OnlyHubOwnerFunction(address caller); error OnlyHubContractsFunction(address caller); + error OnlyProfileAdminFunction(address caller); } diff --git a/contracts/v1/errors/ServiceAgreementErrorsV1.sol b/contracts/v1/errors/ServiceAgreementErrorsV1.sol index 0867f502..e35e2f3c 100644 --- a/contracts/v1/errors/ServiceAgreementErrorsV1.sol +++ b/contracts/v1/errors/ServiceAgreementErrorsV1.sol @@ -10,8 +10,6 @@ library ServiceAgreementErrorsV1 { error ZeroEpochsNumber(); error ZeroTokenAmount(); error ScoreFunctionDoesntExist(uint8 scoreFunctionId); - error TooLowAllowance(uint256 amount); - error TooLowBalance(uint256 amount); error ServiceAgreementHasBeenExpired( bytes32 agreementId, uint256 startTime, diff --git a/contracts/v1/errors/ServiceAgreementErrorsV1U1.sol b/contracts/v1/errors/ServiceAgreementErrorsV1U1.sol index 32c93d30..691fca84 100644 --- a/contracts/v1/errors/ServiceAgreementErrorsV1U1.sol +++ b/contracts/v1/errors/ServiceAgreementErrorsV1U1.sol @@ -11,8 +11,6 @@ library ServiceAgreementErrorsV1U1 { error ZeroTokenAmount(); error ScoreFunctionDoesntExist(uint8 scoreFunctionId); error HashFunctionDoesntExist(uint8 hashFunctionId); - error TooLowAllowance(uint256 amount); - error TooLowBalance(uint256 amount); error ServiceAgreementHasBeenExpired( bytes32 agreementId, uint256 startTime, diff --git a/contracts/v1/errors/TokenErrors.sol b/contracts/v1/errors/TokenErrors.sol new file mode 100644 index 00000000..3e10406c --- /dev/null +++ b/contracts/v1/errors/TokenErrors.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +library TokenErrors { + error TooLowAllowance(address tokenAddress, uint256 amount); + error TooLowBalance(address tokenAddress, uint256 amount); +} diff --git a/contracts/v1/storage/ShardingTableStorage.sol b/contracts/v1/storage/ShardingTableStorage.sol index 965628a9..8bfdf659 100644 --- a/contracts/v1/storage/ShardingTableStorage.sol +++ b/contracts/v1/storage/ShardingTableStorage.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.16; import {HubDependent} from "../abstract/HubDependent.sol"; import {Named} from "../interface/Named.sol"; import {Versioned} from "../interface/Versioned.sol"; -import {ShardingTableStructs} from "../structs/ShardingTableStructs.sol"; +import {ShardingTableStructsV1} from "../structs/ShardingTableStructsV1.sol"; import {NULL} from "../constants/ShardingTableConstants.sol"; contract ShardingTableStorage is Named, Versioned, HubDependent { @@ -17,7 +17,7 @@ contract ShardingTableStorage is Named, Versioned, HubDependent { uint72 public nodesCount; // identityId => Node - mapping(uint72 => ShardingTableStructs.Node) internal nodes; + mapping(uint72 => ShardingTableStructsV1.Node) internal nodes; constructor(address hubAddress) HubDependent(hubAddress) { head = NULL; @@ -49,14 +49,14 @@ contract ShardingTableStorage is Named, Versioned, HubDependent { } function createNodeObject(uint72 identityId, uint72 prevIdentityId, uint72 nextIdentityId) external onlyContracts { - nodes[identityId] = ShardingTableStructs.Node({ + nodes[identityId] = ShardingTableStructsV1.Node({ identityId: identityId, prevIdentityId: prevIdentityId, nextIdentityId: nextIdentityId }); } - function getNode(uint72 identityId) external view returns (ShardingTableStructs.Node memory) { + function getNode(uint72 identityId) external view returns (ShardingTableStructsV1.Node memory) { return nodes[identityId]; } @@ -79,10 +79,10 @@ contract ShardingTableStorage is Named, Versioned, HubDependent { function getMultipleNodes( uint72 firstIdentityId, uint16 nodesNumber - ) external view returns (ShardingTableStructs.Node[] memory) { - ShardingTableStructs.Node[] memory nodesPage = new ShardingTableStructs.Node[](nodesNumber); + ) external view returns (ShardingTableStructsV1.Node[] memory) { + ShardingTableStructsV1.Node[] memory nodesPage = new ShardingTableStructsV1.Node[](nodesNumber); - ShardingTableStructs.Node memory currentNode = nodes[firstIdentityId]; + ShardingTableStructsV1.Node memory currentNode = nodes[firstIdentityId]; for (uint256 i; i < nodesNumber; ) { nodesPage[i] = currentNode; currentNode = nodes[currentNode.nextIdentityId]; diff --git a/contracts/v2/Staking.sol b/contracts/v2/Staking.sol new file mode 100644 index 00000000..f9b1edfc --- /dev/null +++ b/contracts/v2/Staking.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import {ShardingTableV2} from "./ShardingTable.sol"; +import {Shares} from "../v1/Shares.sol"; +import {IdentityStorageV2} from "./storage/IdentityStorage.sol"; +import {ParametersStorage} from "../v1/storage/ParametersStorage.sol"; +import {ProfileStorage} from "../v1/storage/ProfileStorage.sol"; +import {ServiceAgreementStorageProxy} from "../v1/storage/ServiceAgreementStorageProxy.sol"; +import {ShardingTableStorageV2} from "./storage/ShardingTableStorage.sol"; +import {StakingStorage} from "../v1/storage/StakingStorage.sol"; +import {ContractStatus} from "../v1/abstract/ContractStatus.sol"; +import {Initializable} from "../v1/interface/Initializable.sol"; +import {Named} from "../v1/interface/Named.sol"; +import {Versioned} from "../v1/interface/Versioned.sol"; +import {GeneralErrors} from "../v1/errors/GeneralErrors.sol"; +import {TokenErrors} from "../v1/errors/TokenErrors.sol"; +import {ProfileErrors} from "./errors/ProfileErrors.sol"; +import {StakingErrors} from "./errors/StakingErrors.sol"; +import {ADMIN_KEY} from "../v1/constants/IdentityConstants.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract StakingV2 is Named, Versioned, ContractStatus, Initializable { + event StakeIncreased( + uint72 indexed identityId, + bytes nodeId, + address indexed staker, + uint96 oldStake, + uint96 newStake + ); + event StakeWithdrawalStarted( + uint72 indexed identityId, + bytes nodeId, + address indexed staker, + uint96 oldStake, + uint96 newStake, + uint256 withdrawalPeriodEnd + ); + event StakeWithdrawn(uint72 indexed identityId, bytes nodeId, address indexed staker, uint96 withdrawnStakeAmount); + event AccumulatedOperatorFeeIncreased( + uint72 indexed identityId, + bytes nodeId, + uint96 oldAccumulatedOperatorFee, + uint96 newAccumulatedOperatorFee + ); + event OperatorFeeUpdated(uint72 indexed identityId, bytes nodeId, uint8 operatorFee); + + string private constant _NAME = "Staking"; + string private constant _VERSION = "1.0.2"; + + ShardingTableV2 public shardingTableContract; + IdentityStorageV2 public identityStorage; + ParametersStorage public parametersStorage; + ProfileStorage public profileStorage; + StakingStorage public stakingStorage; + ServiceAgreementStorageProxy public serviceAgreementStorageProxy; + ShardingTableStorageV2 public shardingTableStorage; + IERC20 public tokenContract; + + // solhint-disable-next-line no-empty-blocks + constructor(address hubAddress) ContractStatus(hubAddress) {} + + modifier onlyAdmin(uint72 identityId) { + _checkAdmin(identityId); + _; + } + + function initialize() public onlyHubOwner { + shardingTableContract = ShardingTableV2(hub.getContractAddress("ShardingTable")); + identityStorage = IdentityStorageV2(hub.getContractAddress("IdentityStorage")); + parametersStorage = ParametersStorage(hub.getContractAddress("ParametersStorage")); + profileStorage = ProfileStorage(hub.getContractAddress("ProfileStorage")); + stakingStorage = StakingStorage(hub.getContractAddress("StakingStorage")); + serviceAgreementStorageProxy = ServiceAgreementStorageProxy( + hub.getContractAddress("ServiceAgreementStorageProxy") + ); + shardingTableStorage = ShardingTableStorageV2(hub.getContractAddress("ShardingTableStorage")); + tokenContract = IERC20(hub.getContractAddress("Token")); + } + + function name() external pure virtual override returns (string memory) { + return _NAME; + } + + function version() external pure virtual override returns (string memory) { + return _VERSION; + } + + function addStake(address sender, uint72 identityId, uint96 stakeAmount) external onlyContracts { + _addStake(sender, identityId, stakeAmount); + } + + function addStake(uint72 identityId, uint96 stakeAmount) external { + _addStake(msg.sender, identityId, stakeAmount); + } + + function startStakeWithdrawal(uint72 identityId, uint96 sharesToBurn) external { + if (sharesToBurn == 0) { + revert StakingErrors.ZeroSharesAmount(); + } + + ProfileStorage ps = profileStorage; + StakingStorage ss = stakingStorage; + + if (!ps.profileExists(identityId)) { + revert ProfileErrors.ProfileDoesntExist(identityId); + } + + Shares sharesContract = Shares(ps.getSharesContractAddress(identityId)); + + if (sharesToBurn > sharesContract.balanceOf(msg.sender)) { + revert TokenErrors.TooLowBalance(address(sharesContract), sharesContract.balanceOf(msg.sender)); + } + + uint96 oldStake = ss.totalStakes(identityId); + uint96 stakeWithdrawalAmount = uint96((uint256(oldStake) * sharesToBurn) / sharesContract.totalSupply()); + uint96 newStake = oldStake - stakeWithdrawalAmount; + uint96 newStakeWithdrawalAmount = ss.getWithdrawalRequestAmount(identityId, msg.sender) + stakeWithdrawalAmount; + + ParametersStorage params = parametersStorage; + + uint256 withdrawalPeriodEnd = block.timestamp + params.stakeWithdrawalDelay(); + ss.createWithdrawalRequest(identityId, msg.sender, newStakeWithdrawalAmount, withdrawalPeriodEnd); + ss.setTotalStake(identityId, newStake); + sharesContract.burnFrom(msg.sender, sharesToBurn); + + if (shardingTableStorage.nodeExists(identityId) && (newStake < params.minimumStake())) { + shardingTableContract.removeNode(identityId); + } + + emit StakeWithdrawalStarted( + identityId, + ps.getNodeId(identityId), + msg.sender, + oldStake, + newStake, + withdrawalPeriodEnd + ); + } + + function withdrawStake(uint72 identityId) external { + ProfileStorage ps = profileStorage; + + if (!ps.profileExists(identityId)) { + revert ProfileErrors.ProfileDoesntExist(identityId); + } + + StakingStorage ss = stakingStorage; + + uint96 stakeWithdrawalAmount; + uint256 withdrawalTimestamp; + (stakeWithdrawalAmount, withdrawalTimestamp) = ss.withdrawalRequests(identityId, msg.sender); + + if (stakeWithdrawalAmount == 0) { + revert StakingErrors.WithdrawalWasntInitiated(); + } + if (withdrawalTimestamp >= block.timestamp) { + revert StakingErrors.WithdrawalPeriodPending(withdrawalTimestamp); + } + + ss.deleteWithdrawalRequest(identityId, msg.sender); + ss.transferStake(msg.sender, stakeWithdrawalAmount); + + emit StakeWithdrawn(identityId, ps.getNodeId(identityId), msg.sender, stakeWithdrawalAmount); + } + + function addReward(bytes32 agreementId, uint72 identityId, uint96 rewardAmount) external onlyContracts { + ServiceAgreementStorageProxy sasProxy = serviceAgreementStorageProxy; + StakingStorage ss = stakingStorage; + + uint96 operatorFee = (rewardAmount * ss.operatorFees(identityId)) / 100; + uint96 delegatorsReward = rewardAmount - operatorFee; + + ProfileStorage ps = profileStorage; + + uint96 oldAccumulatedOperatorFee = ps.getAccumulatedOperatorFee(identityId); + uint96 oldStake = ss.totalStakes(identityId); + + if (operatorFee != 0) { + ps.setAccumulatedOperatorFee(identityId, oldAccumulatedOperatorFee + operatorFee); + sasProxy.transferAgreementTokens(agreementId, address(ps), operatorFee); + } + + if (delegatorsReward != 0) { + ss.setTotalStake(identityId, oldStake + delegatorsReward); + sasProxy.transferAgreementTokens(agreementId, address(ss), delegatorsReward); + + if (!shardingTableStorage.nodeExists(identityId) && oldStake >= parametersStorage.minimumStake()) { + shardingTableContract.insertNode(identityId); + } + } + + emit AccumulatedOperatorFeeIncreased( + identityId, + ps.getNodeId(identityId), + oldAccumulatedOperatorFee, + oldAccumulatedOperatorFee + operatorFee + ); + + address sasAddress; + if (sasProxy.agreementV1Exists(agreementId)) { + sasAddress = sasProxy.agreementV1StorageAddress(); + } else { + sasAddress = sasProxy.agreementV1U1StorageAddress(); + } + + emit StakeIncreased(identityId, ps.getNodeId(identityId), sasAddress, oldStake, oldStake + delegatorsReward); + } + + // solhint-disable-next-line no-empty-blocks + function slash(uint72 identityId) external onlyContracts { + // TBD + } + + function setOperatorFee(uint72 identityId, uint8 operatorFee) external onlyAdmin(identityId) { + if (operatorFee > 100) { + revert StakingErrors.InvalidOperatorFee(); + } + stakingStorage.setOperatorFee(identityId, operatorFee); + + emit OperatorFeeUpdated(identityId, profileStorage.getNodeId(identityId), operatorFee); + } + + function _addStake(address sender, uint72 identityId, uint96 stakeAmount) internal virtual { + StakingStorage ss = stakingStorage; + ProfileStorage ps = profileStorage; + ParametersStorage params = parametersStorage; + IERC20 tknc = tokenContract; + + uint96 oldStake = ss.totalStakes(identityId); + uint96 newStake = oldStake + stakeAmount; + + if (!ps.profileExists(identityId)) { + revert ProfileErrors.ProfileDoesntExist(identityId); + } + if (stakeAmount > tknc.allowance(sender, address(this))) { + revert TokenErrors.TooLowAllowance(address(tknc), tknc.allowance(sender, address(this))); + } + if (newStake > params.maximumStake()) { + revert StakingErrors.MaximumStakeExceeded(params.maximumStake()); + } + + Shares sharesContract = Shares(ps.getSharesContractAddress(identityId)); + + uint256 sharesMinted; + if (sharesContract.totalSupply() == 0) { + sharesMinted = stakeAmount; + } else { + sharesMinted = ((uint256(stakeAmount) * sharesContract.totalSupply()) / oldStake); + } + sharesContract.mint(sender, sharesMinted); + + ss.setTotalStake(identityId, newStake); + tknc.transferFrom(sender, address(ss), stakeAmount); + + if (!shardingTableStorage.nodeExists(identityId) && newStake >= params.minimumStake()) { + shardingTableContract.insertNode(identityId); + } + + emit StakeIncreased(identityId, ps.getNodeId(identityId), sender, oldStake, newStake); + } + + function _checkAdmin(uint72 identityId) internal view virtual { + if (!identityStorage.keyHasPurpose(identityId, keccak256(abi.encodePacked(msg.sender)), ADMIN_KEY)) { + revert GeneralErrors.OnlyProfileAdminFunction(msg.sender); + } + } +} diff --git a/contracts/v2/errors/ProfileErrors.sol b/contracts/v2/errors/ProfileErrors.sol new file mode 100644 index 00000000..117c9dc5 --- /dev/null +++ b/contracts/v2/errors/ProfileErrors.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +library ProfileErrors { + error ProfileDoesntExist(uint72 identityId); +} diff --git a/contracts/v2/errors/StakingErrors.sol b/contracts/v2/errors/StakingErrors.sol new file mode 100644 index 00000000..78f5e00e --- /dev/null +++ b/contracts/v2/errors/StakingErrors.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +library StakingErrors { + error ZeroSharesAmount(); + error WithdrawalWasntInitiated(); + error WithdrawalPeriodPending(uint256 endTimestamp); + error InvalidOperatorFee(); + error MaximumStakeExceeded(uint256 amount); +}