From 1be8e65439b4ec1655886cb1b035533a357af3cc Mon Sep 17 00:00:00 2001 From: frenchkebab Date: Fri, 23 Aug 2024 20:46:10 +0900 Subject: [PATCH 001/158] Add staking contracts with interfaces --- .../staking/l2_contracts/NativeStaking.sol | 57 +++++++++++ .../staking/l2_contracts/StakingManager.sol | 95 +++++++++++++++++++ .../staking/l2_contracts/SymbioticStaking.sol | 82 ++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 contracts/staking/l2_contracts/NativeStaking.sol create mode 100644 contracts/staking/l2_contracts/StakingManager.sol create mode 100644 contracts/staking/l2_contracts/SymbioticStaking.sol diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol new file mode 100644 index 0000000..671d372 --- /dev/null +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract NativeStaking { + // TODO: token Set + // TODO: checkpoints? + + // TODO: (getter) Operator => token => stakeAmount + mapping(address operator => mapping(address token => uint256 amount)) public stakingAmounts; + + + // Staker should be able to choose an Operator they want to stake into + // This should update StakingManger's state + function stake(address operator, address token, uint256 amount) external { + // TODO: should accept only POND atm, but should have flexibility to accept other tokens + + //?: Will the rewards be tracked off-chain? Or tracked with Checkpoints? + } + + // Operators need to self stake tokens to be able to receive jobs (jobs will be restricted based on self stake amount) + // This should update StakingManger's state + function selfStake(address _token, uint256 amount) external { + // TODO: only operators + } + + // This should update StakingManger's state + function unstake(address operator, address token, uint256 amount) external { + // TODO + } + + /*======================================== Getters ========================================*/ + + // stake of an account for a specific operator + function stakeOf(address account, address operator, address token) external view returns (uint256) { + // TODO + } + + // stake of an account for all operators + function stakeOf(address account, address token) external view returns (uint256) { + // TODO + } + + // TODO: manages the staking information and tokens provided by stakers and delegated to specific operators + + // TODO: functions that can provide the latest staking information for specific users and operators + + + /*======================================== Admin ========================================*/ + + function addToken(address token) external { + // TODO: Admin only + } + + function removeToken(address token) external { + // TODO: admin only + } +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol new file mode 100644 index 0000000..b82a123 --- /dev/null +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract StakingManager { + // TODO: Staking Pool Set + // TODO: Staking Pool flag + + // TODO: integration with Slasher + + + // TODO: Self stake is given to the slasher while the rest of the stakers is burnt when slashed + // TODO: check necessary params + function slashJob(address _jobId, address _vault, uint256 _captureTimestamp, uint256 _amount, address _rewardAddress) external { + // TODO: only slashingManager + } + + // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) + // locked stake will be unlocked after an epoch if no slas result is submitted + function onJobCreation(address _jobId, address _operator, uint256 _amount) external { + // TODO: only jobManager + } + + // called when job is completed to unlock the locked stakes + function onJobCompletion(address _jobId) external { + // TODO: only jobManager + } + + // called when Staked/Unstaked in the Staking Pool (Native Staking, Symbiotic Staking) + function updateStake(address operator, address token, uint256 amount) external { + // TODO: only Staking Pool + } + + // when certain period has passed after the lock and no slash result is submitted, this can be unlocked + function unlockStake(address _jobId) external { } + + + /*======================================== Getters ========================================*/ + + // check if the job is slashable and can be sent to the slashing manager + // this only tells if the deadline for proof submission has passed + // so even when this function returns true and transaction submitted to L1 can be reverted + // when someone already has submitted the proof + function isSlashable(address _jobId) external view returns(bool) { + // TODO + } + + // for all operators + function getLatestStakeInfo() public { + // TODO + } + + function getLatestStakeInfoAt(uint256 timestamp) public { + // TODO + } + + // for a specific operator + function getLatestStakeInfo(address operator) public { + // TODO + } + + function getStakeInfoAt(address operator, address token, uint256 timestamp) public returns(uint256) { + // TODO + } + + // TODO: function that consolidates the staking information from both Native Staking and Symbiotic Staking and returns the total stake amount + + /*======================================== Admin ========================================*/ + + // add new staking pool + function addStakingPool(address _stakingPool) external { + // TODO: onlyAdmin + } + + function removeStakingPool(address _stakingPool) external { + // TODO: onlyAdmin + } + + function setStakingPoolStatus(address _stakingPool, bool _status) external { + // TODO: onlyAdmin + } + + function setSlashingManager(address _slashingManager) external { + // TODO: only admin + } + + // TODO: integration with JobManager + function setJobManager(address _jobManager) external { + // TODO: only admin + } + + // TODO: interaction with Price Oracle + function setPriceOracle(address _priceOracle) external { + // TODO + } +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol new file mode 100644 index 0000000..4eb659e --- /dev/null +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract SymbioticStaking { + // TODO: address Operator => address token => CheckPoints.Trace256 stakeAmount (Question: operators' stake amount is consolidated within same vault?) + + // TODO: set SD + + // TODO: set TC + + // TODO: lastCapturedTimestamp + + //? How to manage Vault lists? + + // Transmitter submits staking data snapshot + // This should update StakingManger's state + function submitSnapshot( + uint256 txIndex, + uint256 noOfTxs, + uint256 captureTimestamp, + bytes memory stakeData, + bytes memory signature + ) external { + // TODO: check) captureTimestamp >= lastCaptureTimestamp + SD + + // TODO: Check) noOfTxs txIndex + + // TODO: Check) noOfTxs should be consistent across all the partial snapshots + + // TODO: Data transmitter should get TC% of the rewards + + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + + // TODO: stakeData should be of the correct format which has key value pairs of operators and stakeDelta + + // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + + // TODO: Should update the latest complete snapshot information once the last chunk of staking snapshot is received (Updates TC based on the delay) + } + + /*======================================== Getters ========================================*/ + + function getLatestStakingAmount() external view returns (address[] memory tokens, uint256[] memory amounts) { + // TODO + } + + function getStakingAmountAt(uint256 timestamp) external view returns (uint256 amount) { + // TODO + } + + function getLatestStakingAmount(address token) external view returns (uint256 amount) { + // TODO + } + + function getStakingAmountAt(address token, uint256 timestamp) external view returns (uint256 amount) { + // TODO + } + + // returns latest stake amount of an Operator for all tokens + function getLatestOperatorStakingAmount(address operator) external view returns (address[] memory tokens, uint256[] memory amounts) { + // TODO + } + + // returns latest stake amount of an Operator for a specific token + function getLatestOperatorStakingAmount(address operator, address token) external view returns (uint256 amount) { + // TODO + } + + // returns stake amounts of an Operator for all tokens at a specific timestamp + function getOperatorStakingAmountAt(address operator, uint256 timestamp) external view returns (address[] memory tokens, uint256[] memory amounts ) { + // TODO + } + + // returns stake amount of an Operator for a specific token at a specific timestamp + function getOperatorStakingAmountAt(address operator, address token, uint256 timestamp) external view returns (uint256 amount) { + // TODO + } + + function slashSymbioticVault(address operator, address vault, uint256 captureTimestamp, uint256 amount, address rewardAddress) external { + // TODO only slashingManager + } +} From c8dedb35d5807dd547121dfb9075ce3f1f34baa9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 28 Aug 2024 19:04:47 +0900 Subject: [PATCH 002/158] add Enumerable Set for tokens --- .../staking/l2_contracts/NativeStaking.sol | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 671d372..1a9cd11 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -1,12 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + contract NativeStaking { + using EnumerableSet for EnumerableSet.AddressSet; + // TODO: token Set + EnumerableSet.AddressSet private tokenSet; + // TODO: checkpoints? - // TODO: (getter) Operator => token => stakeAmount - mapping(address operator => mapping(address token => uint256 amount)) public stakingAmounts; + mapping(address operator => mapping(address token => uint256 amount)) public stakes; + + function stakeOf(address _operator, address _token) external view returns (uint256) { + return stakes[_operator][_token]; + } + + // function stakesOf(address _operator) external view returns (uint256[] memory, uint256[] memory amounts) { + // uint256 len = tokens.length; + // } // Staker should be able to choose an Operator they want to stake into @@ -31,12 +44,12 @@ contract NativeStaking { /*======================================== Getters ========================================*/ // stake of an account for a specific operator - function stakeOf(address account, address operator, address token) external view returns (uint256) { + function getStake(address account, address operator, address token) external view returns (uint256) { // TODO } // stake of an account for all operators - function stakeOf(address account, address token) external view returns (uint256) { + function getStakes(address account, address token) external view returns (uint256) { // TODO } @@ -49,9 +62,13 @@ contract NativeStaking { function addToken(address token) external { // TODO: Admin only + + // TODO: token should be added in StakingManager as well } function removeToken(address token) external { // TODO: admin only + + // TODO: token should be added in StakingManager as well } } \ No newline at end of file From 67b6c38f445217103924270704fe912d40c0fe11 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 28 Aug 2024 19:48:00 +0900 Subject: [PATCH 003/158] add getter for stake amount --- .../staking/l2_contracts/NativeStaking.sol | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 1a9cd11..cdb44c7 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -6,21 +6,34 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet contract NativeStaking { using EnumerableSet for EnumerableSet.AddressSet; + error UnsupportedToken(); + // TODO: token Set - EnumerableSet.AddressSet private tokenSet; + EnumerableSet.AddressSet private tokens; // TODO: checkpoints? mapping(address operator => mapping(address token => uint256 amount)) public stakes; - function stakeOf(address _operator, address _token) external view returns (uint256) { + modifier onlySupportedToken(address _token) { + require(tokens.contains(_token), UnsupportedToken()); + _; + } + + // Returns the amount of a token staked by the operator + function stakeOf(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { return stakes[_operator][_token]; } - // function stakesOf(address _operator) external view returns (uint256[] memory, uint256[] memory amounts) { - // uint256 len = tokens.length; - // } - + // Returns the list of tokens staked by the operator and the amounts + function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + uint256 len = tokens.length(); + + for (uint256 i = 0; i < len; i++) { + _tokens[i] = tokens.at(i); + _amounts[i] = stakes[_operator][tokens.at(i)]; + } + } // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state @@ -47,7 +60,7 @@ contract NativeStaking { function getStake(address account, address operator, address token) external view returns (uint256) { // TODO } - + // stake of an account for all operators function getStakes(address account, address token) external view returns (uint256) { // TODO @@ -56,7 +69,6 @@ contract NativeStaking { // TODO: manages the staking information and tokens provided by stakers and delegated to specific operators // TODO: functions that can provide the latest staking information for specific users and operators - /*======================================== Admin ========================================*/ @@ -71,4 +83,4 @@ contract NativeStaking { // TODO: token should be added in StakingManager as well } -} \ No newline at end of file +} From bf7fdb6d577b5a5ee0be750d6f7c413780ed36af Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 15:37:38 +0900 Subject: [PATCH 004/158] add contract inheritance --- .../staking/l2_contracts/NativeStaking.sol | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index cdb44c7..f32fcc6 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -2,8 +2,19 @@ pragma solidity ^0.8.26; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -contract NativeStaking { +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +contract NativeStaking is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable +{ using EnumerableSet for EnumerableSet.AddressSet; error UnsupportedToken(); @@ -20,6 +31,14 @@ contract NativeStaking { _; } + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165Upgradeable, AccessControlUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + // Returns the amount of a token staked by the operator function stakeOf(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { return stakes[_operator][_token]; From 27fafcaad2e19510fe5c8f57b04f10b9a790ce83 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 15:45:10 +0900 Subject: [PATCH 005/158] add add/remove token feature --- .../staking/l2_contracts/NativeStaking.sol | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index f32fcc6..1347faa 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -39,6 +39,15 @@ contract NativeStaking is function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + function initialize(address _admin) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + // Returns the amount of a token staked by the operator function stakeOf(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { return stakes[_operator][_token]; @@ -91,15 +100,11 @@ contract NativeStaking is /*======================================== Admin ========================================*/ - function addToken(address token) external { - // TODO: Admin only - - // TODO: token should be added in StakingManager as well + function addToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(tokens.add(token), "Token already exists"); } - function removeToken(address token) external { - // TODO: admin only - - // TODO: token should be added in StakingManager as well + function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(tokens.remove(token), "Token does not exist"); } } From f4441fd2f65f92d4a921bdf512984e60e1641b0d Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 16:18:55 +0900 Subject: [PATCH 006/158] add supportedSignature feature --- contracts/staking/l2_contracts/NativeStaking.sol | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 1347faa..2f1d466 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -25,12 +25,19 @@ contract NativeStaking is // TODO: checkpoints? mapping(address operator => mapping(address token => uint256 amount)) public stakes; + + mapping(bytes4 sig => bool isSupported) public supportedSignatures; modifier onlySupportedToken(address _token) { require(tokens.contains(_token), UnsupportedToken()); _; } + modifier onlySupportedSignature(bytes4 sig) { + require(supportedSignatures[sig], "Function not supported"); + _; + } + function supportsInterface( bytes4 interfaceId ) public view virtual override(ERC165Upgradeable, AccessControlUpgradeable) returns (bool) { @@ -65,7 +72,7 @@ contract NativeStaking is // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state - function stake(address operator, address token, uint256 amount) external { + function stake(address operator, address token, uint256 amount) external onlySupportedSignature(msg.sig) { // TODO: should accept only POND atm, but should have flexibility to accept other tokens //?: Will the rewards be tracked off-chain? Or tracked with Checkpoints? @@ -107,4 +114,8 @@ contract NativeStaking is function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { require(tokens.remove(token), "Token does not exist"); } + + function setSupportedSignature(bytes4 sig, bool isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { + supportedSignatures[sig] = isSupported; + } } From 3b6dbcac1f2c53536b22278c9004b3d51517a848 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 16:33:45 +0900 Subject: [PATCH 007/158] implement stake/operatorSelfStake logic --- .../staking/l2_contracts/NativeStaking.sol | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 2f1d466..4338df6 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -17,19 +17,18 @@ contract NativeStaking is { using EnumerableSet for EnumerableSet.AddressSet; - error UnsupportedToken(); - - // TODO: token Set EnumerableSet.AddressSet private tokens; - // TODO: checkpoints? + event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); mapping(address operator => mapping(address token => uint256 amount)) public stakes; mapping(bytes4 sig => bool isSupported) public supportedSignatures; modifier onlySupportedToken(address _token) { - require(tokens.contains(_token), UnsupportedToken()); + require(tokens.contains(_token), "Token not supported"); _; } @@ -72,16 +71,18 @@ contract NativeStaking is // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state - function stake(address operator, address token, uint256 amount) external onlySupportedSignature(msg.sig) { - // TODO: should accept only POND atm, but should have flexibility to accept other tokens + function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) { + stakes[_operator][_token] += _amount; - //?: Will the rewards be tracked off-chain? Or tracked with Checkpoints? + emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } // Operators need to self stake tokens to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state - function selfStake(address _token, uint256 amount) external { - // TODO: only operators + function operatorSelfStake(address _operator, address _token, uint256 _amount) external { + stakes[_operator][_token] += _amount; + + emit SelfStaked(_operator, _token, _amount, block.timestamp); } // This should update StakingManger's state From 215b33c721e93f95ad7af153759b309ba899da93 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 16:37:39 +0900 Subject: [PATCH 008/158] add interfaces --- .../interfaces/staking/IKalypsoStaking.sol | 6 +++++ .../interfaces/{ => staking}/IL2Staking.sol | 4 ++-- .../interfaces/staking/INativeStaking.sol | 16 ++++++++++++++ .../interfaces/staking/IStakingManager.sol | 0 .../interfaces/staking/ISymbioticStaking.sol | 0 .../staking/l2_contracts/NativeStaking.sol | 22 +++++++++---------- 6 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 contracts/interfaces/staking/IKalypsoStaking.sol rename contracts/interfaces/{ => staking}/IL2Staking.sol (100%) create mode 100644 contracts/interfaces/staking/INativeStaking.sol create mode 100644 contracts/interfaces/staking/IStakingManager.sol create mode 100644 contracts/interfaces/staking/ISymbioticStaking.sol diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol new file mode 100644 index 0000000..5698fc7 --- /dev/null +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +interface IKalypsoStaking { + function stakeOf(address _operator, address _token) external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/interfaces/IL2Staking.sol b/contracts/interfaces/staking/IL2Staking.sol similarity index 100% rename from contracts/interfaces/IL2Staking.sol rename to contracts/interfaces/staking/IL2Staking.sol index 4513590..de58d08 100644 --- a/contracts/interfaces/IL2Staking.sol +++ b/contracts/interfaces/staking/IL2Staking.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.0; interface IL2Staking { - function stake(address generatorAddress, uint256 amount) external returns (uint256); - function intendToReduceStake(uint256 stakeToReduce) external; + function stake(address generatorAddress, uint256 amount) external returns (uint256); + function unstake(address receiver) external; } diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol new file mode 100644 index 0000000..d5217d3 --- /dev/null +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; + +interface INativeStaking is IKalypsoStaking { + event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + + function stakeOf(address _operator, address _token) external view returns (uint256); + + function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); + + function supportedSignatures(bytes4 sig) external view returns (bool); +} \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 4338df6..61b30a1 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -8,20 +8,22 @@ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/acce import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; + contract NativeStaking is ContextUpgradeable, ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, - ReentrancyGuardUpgradeable + ReentrancyGuardUpgradeable, + INativeStaking { using EnumerableSet for EnumerableSet.AddressSet; EnumerableSet.AddressSet private tokens; - event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + // TODO: check if timestamp is needed + mapping(address operator => mapping(address token => uint256 amount)) public stakes; @@ -71,7 +73,7 @@ contract NativeStaking is // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state - function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) { + function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { stakes[_operator][_token] += _amount; emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); @@ -79,15 +81,15 @@ contract NativeStaking is // Operators need to self stake tokens to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state - function operatorSelfStake(address _operator, address _token, uint256 _amount) external { + function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { stakes[_operator][_token] += _amount; emit SelfStaked(_operator, _token, _amount, block.timestamp); } // This should update StakingManger's state - function unstake(address operator, address token, uint256 amount) external { - // TODO + function unstake(address operator, address token, uint256 amount) external nonReentrant { + } /*======================================== Getters ========================================*/ @@ -102,10 +104,6 @@ contract NativeStaking is // TODO } - // TODO: manages the staking information and tokens provided by stakers and delegated to specific operators - - // TODO: functions that can provide the latest staking information for specific users and operators - /*======================================== Admin ========================================*/ function addToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { From 78f02d03fe241d1cf98b6ec64832f65b17685ada Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 20:32:35 +0900 Subject: [PATCH 009/158] add getter for signature support --- .../interfaces/{staking => }/IL2Staking.sol | 0 .../interfaces/staking/INativeStaking.sol | 8 ++++- .../staking/l2_contracts/NativeStaking.sol | 30 +++++++++++-------- 3 files changed, 24 insertions(+), 14 deletions(-) rename contracts/interfaces/{staking => }/IL2Staking.sol (100%) diff --git a/contracts/interfaces/staking/IL2Staking.sol b/contracts/interfaces/IL2Staking.sol similarity index 100% rename from contracts/interfaces/staking/IL2Staking.sol rename to contracts/interfaces/IL2Staking.sol diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index d5217d3..0ebf5cb 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,6 +4,12 @@ pragma solidity ^0.8.26; import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; interface INativeStaking is IKalypsoStaking { + struct StakeInfo { + uint256 sakteAmount; + uint256 selfStakeAmount; + } + + // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); @@ -12,5 +18,5 @@ interface INativeStaking is IKalypsoStaking { function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); - function supportedSignatures(bytes4 sig) external view returns (bool); + function isSupportedSignature(bytes4 sig) external view returns (bool); } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 61b30a1..f203dfb 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -20,17 +20,17 @@ contract NativeStaking is { using EnumerableSet for EnumerableSet.AddressSet; - EnumerableSet.AddressSet private tokens; - - // TODO: check if timestamp is needed - + EnumerableSet.AddressSet private tokenSet; + EnumerableSet.AddressSet private operatorSet; + mapping(address operator => mapping(address token => uint256 amount)) public selfStakes; mapping(address operator => mapping(address token => uint256 amount)) public stakes; + + mapping(bytes4 sig => bool isSupported) private supportedSignatures; - mapping(bytes4 sig => bool isSupported) public supportedSignatures; modifier onlySupportedToken(address _token) { - require(tokens.contains(_token), "Token not supported"); + require(tokenSet.contains(_token), "Token not supported"); _; } @@ -61,13 +61,13 @@ contract NativeStaking is return stakes[_operator][_token]; } - // Returns the list of tokens staked by the operator and the amounts + // Returns the list of tokenSet staked by the operator and the amounts function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { - uint256 len = tokens.length(); + uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { - _tokens[i] = tokens.at(i); - _amounts[i] = stakes[_operator][tokens.at(i)]; + _tokens[i] = tokenSet.at(i); + _amounts[i] = stakes[_operator][tokenSet.at(i)]; } } @@ -79,7 +79,7 @@ contract NativeStaking is emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } - // Operators need to self stake tokens to be able to receive jobs (jobs will be restricted based on self stake amount) + // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { stakes[_operator][_token] += _amount; @@ -104,14 +104,18 @@ contract NativeStaking is // TODO } + function isSupportedSignature(bytes4 sig) external view returns (bool) { + return supportedSignatures[sig]; + } + /*======================================== Admin ========================================*/ function addToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(tokens.add(token), "Token already exists"); + require(tokenSet.add(token), "Token already exists"); } function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(tokens.remove(token), "Token does not exist"); + require(tokenSet.remove(token), "Token does not exist"); } function setSupportedSignature(bytes4 sig, bool isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { From 9936d1c2ce1c6523d1e81692fe8bf9a9bccb799e Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 20:41:27 +0900 Subject: [PATCH 010/158] add getters for stake --- .../interfaces/staking/IKalypsoStaking.sol | 2 +- .../interfaces/staking/INativeStaking.sol | 10 ++--- .../staking/l2_contracts/NativeStaking.sol | 43 +++++++++++++++---- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index 5698fc7..fa2cd95 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -2,5 +2,5 @@ pragma solidity ^0.8.26; interface IKalypsoStaking { - function stakeOf(address _operator, address _token) external view returns (uint256); + // function stakeOf(address _operator, address _token) external view returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 0ebf5cb..c375150 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -5,8 +5,8 @@ import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; interface INativeStaking is IKalypsoStaking { struct StakeInfo { - uint256 sakteAmount; - uint256 selfStakeAmount; + uint256 delegatedStake; + uint256 selfStake; } // TODO: check if timestamp is needed @@ -14,9 +14,9 @@ interface INativeStaking is IKalypsoStaking { event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - function stakeOf(address _operator, address _token) external view returns (uint256); + // function stakeOf(address _operator, address _token) external view returns (uint256); - function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); + // function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); - function isSupportedSignature(bytes4 sig) external view returns (bool); + // function isSupportedSignature(bytes4 sig) external view returns (bool); } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index f203dfb..c109383 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -23,8 +23,7 @@ contract NativeStaking is EnumerableSet.AddressSet private tokenSet; EnumerableSet.AddressSet private operatorSet; - mapping(address operator => mapping(address token => uint256 amount)) public selfStakes; - mapping(address operator => mapping(address token => uint256 amount)) public stakes; + mapping(address operator => mapping(address token => StakeInfo)) public stakeInfo; // stakeAmount, selfStakeAmount mapping(bytes4 sig => bool isSupported) private supportedSignatures; @@ -57,24 +56,52 @@ contract NativeStaking is } // Returns the amount of a token staked by the operator - function stakeOf(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { - return stakes[_operator][_token]; + function getDelegatedStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + return stakeInfo[_operator][_token].delegatedStake; } // Returns the list of tokenSet staked by the operator and the amounts - function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + function getDelegatedStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { _tokens[i] = tokenSet.at(i); - _amounts[i] = stakes[_operator][tokenSet.at(i)]; + _amounts[i] = stakeInfo[_operator][_tokens[i]].delegatedStake; + } + } + + function getSelfStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + return stakeInfo[_operator][_token].selfStake; + } + + function getSelfStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + uint256 len = tokenSet.length(); + + for (uint256 i = 0; i < len; i++) { + _tokens[i] = tokenSet.at(i); + _amounts[i] = stakeInfo[_operator][_tokens[i]].selfStake; + } + } + + function getTotalStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + StakeInfo memory _stakeInfo = stakeInfo[_operator][_token]; + return _stakeInfo.delegatedStake + _stakeInfo.selfStake; + } + + function getTotalStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + uint256 len = tokenSet.length(); + + for (uint256 i = 0; i < len; i++) { + StakeInfo memory _stakeInfo = stakeInfo[_operator][tokenSet.at(i)]; + _tokens[i] = tokenSet.at(i); + _amounts[i] = _stakeInfo.delegatedStake + _stakeInfo.selfStake; } } // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - stakes[_operator][_token] += _amount; + stakeInfo[_operator][_token].delegatedStake += _amount; emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -82,7 +109,7 @@ contract NativeStaking is // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - stakes[_operator][_token] += _amount; + stakeInfo[_operator][_token].selfStake += _amount; emit SelfStaked(_operator, _token, _amount, block.timestamp); } From 9126919329bd05ccd806f04202768b49ac244c94 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 20:42:37 +0900 Subject: [PATCH 011/158] add unstake --- contracts/staking/l2_contracts/NativeStaking.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index c109383..794165b 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -116,7 +116,9 @@ contract NativeStaking is // This should update StakingManger's state function unstake(address operator, address token, uint256 amount) external nonReentrant { - + stakeInfo[operator][token].delegatedStake -= amount; + + emit Unstaked(msg.sender, operator, token, amount, block.timestamp); } /*======================================== Getters ========================================*/ From 5f873f64bcdd02022e7c96aebc721c68cabfbb3d Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 20:49:04 +0900 Subject: [PATCH 012/158] add userStakeInfo mapping --- .../interfaces/staking/INativeStaking.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 38 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index c375150..45bbf75 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; interface INativeStaking is IKalypsoStaking { - struct StakeInfo { + struct OperatorStakeInfo { uint256 delegatedStake; uint256 selfStake; } diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 794165b..1c31471 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -23,7 +23,8 @@ contract NativeStaking is EnumerableSet.AddressSet private tokenSet; EnumerableSet.AddressSet private operatorSet; - mapping(address operator => mapping(address token => StakeInfo)) public stakeInfo; // stakeAmount, selfStakeAmount + mapping(address operator => mapping(address token => OperatorStakeInfo stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount + mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public userStakeInfo; mapping(bytes4 sig => bool isSupported) private supportedSignatures; @@ -55,9 +56,9 @@ contract NativeStaking is _grantRole(DEFAULT_ADMIN_ROLE, _admin); } - // Returns the amount of a token staked by the operator + function getDelegatedStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { - return stakeInfo[_operator][_token].delegatedStake; + return operatorStakeInfo[_operator][_token].delegatedStake; } // Returns the list of tokenSet staked by the operator and the amounts @@ -66,12 +67,12 @@ contract NativeStaking is for (uint256 i = 0; i < len; i++) { _tokens[i] = tokenSet.at(i); - _amounts[i] = stakeInfo[_operator][_tokens[i]].delegatedStake; + _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].delegatedStake; } } function getSelfStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { - return stakeInfo[_operator][_token].selfStake; + return operatorStakeInfo[_operator][_token].selfStake; } function getSelfStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { @@ -79,29 +80,40 @@ contract NativeStaking is for (uint256 i = 0; i < len; i++) { _tokens[i] = tokenSet.at(i); - _amounts[i] = stakeInfo[_operator][_tokens[i]].selfStake; + _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].selfStake; } } - function getTotalStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { - StakeInfo memory _stakeInfo = stakeInfo[_operator][_token]; + function getOperatorTotalStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + OperatorStakeInfo memory _stakeInfo = operatorStakeInfo[_operator][_token]; return _stakeInfo.delegatedStake + _stakeInfo.selfStake; } - function getTotalStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + function getOperatorTotalStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { - StakeInfo memory _stakeInfo = stakeInfo[_operator][tokenSet.at(i)]; + OperatorStakeInfo memory _stakeInfo = operatorStakeInfo[_operator][tokenSet.at(i)]; _tokens[i] = tokenSet.at(i); _amounts[i] = _stakeInfo.delegatedStake + _stakeInfo.selfStake; } } + function getTokenTotalStake(address _token) external view onlySupportedToken(_token) returns (uint256) { + uint256 len = operatorSet.length(); + uint256 totalStake; + + for (uint256 i = 0; i < len; i++) { + totalStake += operatorStakeInfo[operatorSet.at(i)][_token].delegatedStake + operatorStakeInfo[operatorSet.at(i)][_token].selfStake; + } + + return totalStake; + } + // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - stakeInfo[_operator][_token].delegatedStake += _amount; + operatorStakeInfo[_operator][_token].delegatedStake += _amount; emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -109,14 +121,14 @@ contract NativeStaking is // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - stakeInfo[_operator][_token].selfStake += _amount; + operatorStakeInfo[_operator][_token].selfStake += _amount; emit SelfStaked(_operator, _token, _amount, block.timestamp); } // This should update StakingManger's state function unstake(address operator, address token, uint256 amount) external nonReentrant { - stakeInfo[operator][token].delegatedStake -= amount; + operatorStakeInfo[operator][token].delegatedStake -= amount; emit Unstaked(msg.sender, operator, token, amount, block.timestamp); } From 657119cfcfcf0506f99c5a768bfaa6a784acd154 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 21:06:31 +0900 Subject: [PATCH 013/158] add token transfer logic --- .../interfaces/staking/INativeStaking.sol | 3 +- .../staking/l2_contracts/NativeStaking.sol | 88 +++++++++++-------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 45bbf75..bc25f06 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -12,7 +12,8 @@ interface INativeStaking is IKalypsoStaking { // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - event Unstaked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event StakeWithdrawn(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); + event SelfStakeWithdrawn(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); // function stakeOf(address _operator, address _token) external view returns (uint256); diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 1c31471..fcc8840 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -7,9 +7,15 @@ import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/intro import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + + + import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; + contract NativeStaking is ContextUpgradeable, ERC165Upgradeable, @@ -19,12 +25,13 @@ contract NativeStaking is INativeStaking { using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; EnumerableSet.AddressSet private tokenSet; EnumerableSet.AddressSet private operatorSet; mapping(address operator => mapping(address token => OperatorStakeInfo stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount - mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public userStakeInfo; + mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public userStakeInfo; mapping(bytes4 sig => bool isSupported) private supportedSignatures; @@ -56,6 +63,50 @@ contract NativeStaking is _grantRole(DEFAULT_ADMIN_ROLE, _admin); } + // Staker should be able to choose an Operator they want to stake into + // This should update StakingManger's state + function stake(address _account, address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { + IERC20(_token).safeTransferFrom(_account, address(this), _amount); + + userStakeInfo[_account][_operator][_token] += _amount; + operatorStakeInfo[_operator][_token].delegatedStake += _amount; + + emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); + } + + // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) + // This should update StakingManger's state + function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { + IERC20(_token).safeTransferFrom(_operator, address(this), _amount); + + operatorStakeInfo[_operator][_token].selfStake += _amount; + + emit SelfStaked(_operator, _token, _amount, block.timestamp); + } + + // This should update StakingManger's state + function withdrawStake(address operator, address token, uint256 amount) external nonReentrant { + require(userStakeInfo[msg.sender][operator][token] >= amount, "Insufficient stake"); + + IERC20(token).safeTransfer(msg.sender, amount); + + userStakeInfo[msg.sender][operator][token] -= amount; + operatorStakeInfo[operator][token].delegatedStake -= amount; + + emit StakeWithdrawn(msg.sender, operator, token, amount, block.timestamp); + } + + function withdrawSelfStake(address operator, address token, uint256 amount) external nonReentrant { + require(operatorStakeInfo[operator][token].selfStake >= amount, "Insufficient selfstake"); + + IERC20(token).safeTransfer(operator, amount); + + operatorStakeInfo[operator][token].selfStake -= amount; + + emit SelfStakeWithdrawn(operator, token, amount, block.timestamp); + } + + /*======================================== Getters ========================================*/ function getDelegatedStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { return operatorStakeInfo[_operator][_token].delegatedStake; @@ -110,41 +161,6 @@ contract NativeStaking is return totalStake; } - // Staker should be able to choose an Operator they want to stake into - // This should update StakingManger's state - function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - operatorStakeInfo[_operator][_token].delegatedStake += _amount; - - emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); - } - - // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) - // This should update StakingManger's state - function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - operatorStakeInfo[_operator][_token].selfStake += _amount; - - emit SelfStaked(_operator, _token, _amount, block.timestamp); - } - - // This should update StakingManger's state - function unstake(address operator, address token, uint256 amount) external nonReentrant { - operatorStakeInfo[operator][token].delegatedStake -= amount; - - emit Unstaked(msg.sender, operator, token, amount, block.timestamp); - } - - /*======================================== Getters ========================================*/ - - // stake of an account for a specific operator - function getStake(address account, address operator, address token) external view returns (uint256) { - // TODO - } - - // stake of an account for all operators - function getStakes(address account, address token) external view returns (uint256) { - // TODO - } - function isSupportedSignature(bytes4 sig) external view returns (bool) { return supportedSignatures[sig]; } From 170567c4ccf7a26692cdab094e36caef9febb9f5 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 21:08:58 +0900 Subject: [PATCH 014/158] todos --- contracts/staking/l2_contracts/NativeStaking.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index fcc8840..dd53996 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -88,6 +88,10 @@ contract NativeStaking is function withdrawStake(address operator, address token, uint256 amount) external nonReentrant { require(userStakeInfo[msg.sender][operator][token] >= amount, "Insufficient stake"); + // TODO: check locked time + + // TODO: read from staking manager and calculate withdrawable amount + IERC20(token).safeTransfer(msg.sender, amount); userStakeInfo[msg.sender][operator][token] -= amount; @@ -178,4 +182,6 @@ contract NativeStaking is function setSupportedSignature(bytes4 sig, bool isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { supportedSignatures[sig] = isSupported; } + + // TODO: set staking manager } From c8928f158d506a09af75c53cbed5f3fd614bc864 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 22:10:23 +0900 Subject: [PATCH 015/158] add check logic for submitSnapshot --- .../staking/l2_contracts/SymbioticStaking.sol | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 4eb659e..1ca3dbd 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -5,78 +5,56 @@ contract SymbioticStaking { // TODO: address Operator => address token => CheckPoints.Trace256 stakeAmount (Question: operators' stake amount is consolidated within same vault?) // TODO: set SD + uint256 SD; // TODO: set TC // TODO: lastCapturedTimestamp + uint256 lastCapturedTimestamp; //? How to manage Vault lists? + + struct SnapshotInfo { + uint256 count; + uint256 length; + } + + mapping(uint256 captureTimestamp => mapping(address account => SnapshotInfo snapshot)) snapshotInfo; // Transmitter submits staking data snapshot // This should update StakingManger's state function submitSnapshot( - uint256 txIndex, - uint256 noOfTxs, + uint256 index, + uint256 length, // number of total transactions uint256 captureTimestamp, bytes memory stakeData, bytes memory signature ) external { - // TODO: check) captureTimestamp >= lastCaptureTimestamp + SD - - // TODO: Check) noOfTxs txIndex + require(block.timestamp >= lastCapturedTimestamp + SD, "Cooldown period not passed"); - // TODO: Check) noOfTxs should be consistent across all the partial snapshots + require(length > 0, "Invalid length"); - // TODO: Data transmitter should get TC% of the rewards + require(index < length, "Invalid index"); - // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + require(snapshotInfo[captureTimestamp][msg.sender].count > 0, "Snapshot fully submitted already"); + snapshotInfo[captureTimestamp][msg.sender].count--; - // TODO: stakeData should be of the correct format which has key value pairs of operators and stakeDelta + require(snapshotInfo[captureTimestamp][msg.sender].length == length, "Invalid length"); - // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + // TODO: Verify the signature + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image - // TODO: Should update the latest complete snapshot information once the last chunk of staking snapshot is received (Updates TC based on the delay) - } + // TODO: Data transmitter should get TC% of the rewards - /*======================================== Getters ========================================*/ + // TODO: stakeData should be of the correct format which has key value pairs of operators and stakeDelta (?) - function getLatestStakingAmount() external view returns (address[] memory tokens, uint256[] memory amounts) { - // TODO - } + // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" - function getStakingAmountAt(uint256 timestamp) external view returns (uint256 amount) { - // TODO - } - - function getLatestStakingAmount(address token) external view returns (uint256 amount) { - // TODO + // TODO: Should update the latest complete snapshot information once the last chunk of staking snapshot is received (Updates TC based on the delay) } - function getStakingAmountAt(address token, uint256 timestamp) external view returns (uint256 amount) { - // TODO - } - // returns latest stake amount of an Operator for all tokens - function getLatestOperatorStakingAmount(address operator) external view returns (address[] memory tokens, uint256[] memory amounts) { - // TODO - } - - // returns latest stake amount of an Operator for a specific token - function getLatestOperatorStakingAmount(address operator, address token) external view returns (uint256 amount) { - // TODO - } + /*======================================== Getters ========================================*/ - // returns stake amounts of an Operator for all tokens at a specific timestamp - function getOperatorStakingAmountAt(address operator, uint256 timestamp) external view returns (address[] memory tokens, uint256[] memory amounts ) { - // TODO - } - // returns stake amount of an Operator for a specific token at a specific timestamp - function getOperatorStakingAmountAt(address operator, address token, uint256 timestamp) external view returns (uint256 amount) { - // TODO - } - - function slashSymbioticVault(address operator, address vault, uint256 captureTimestamp, uint256 amount, address rewardAddress) external { - // TODO only slashingManager - } } From 4ba1ee453643154c0591cbfb3352d6c1fb9f4381 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 29 Aug 2024 23:45:46 +0900 Subject: [PATCH 016/158] implement submitSnapshot logic --- .../staking/l2_contracts/SymbioticStaking.sol | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 1ca3dbd..be0c6ca 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -10,49 +10,72 @@ contract SymbioticStaking { // TODO: set TC // TODO: lastCapturedTimestamp - uint256 lastCapturedTimestamp; + uint256 lastCaptureTimestamp; //? How to manage Vault lists? - struct SnapshotInfo { + struct SnapshotTxInfo { uint256 count; uint256 length; } - mapping(uint256 captureTimestamp => mapping(address account => SnapshotInfo snapshot)) snapshotInfo; + struct OperatorSnapshot { + address operator; + uint256 stake; + } + + struct VaultSnapshot { + address vault; + uint256 stake; + } + + mapping(uint256 captureTimestamp => mapping(address account => SnapshotTxInfo snapshot)) submissionInfo; + + // TODO: mappings for operator and vault snapshots // Transmitter submits staking data snapshot // This should update StakingManger's state function submitSnapshot( - uint256 index, - uint256 length, // number of total transactions - uint256 captureTimestamp, - bytes memory stakeData, - bytes memory signature + uint256 _index, + uint256 _length, // number of total transactions + uint256 _captureTimestamp, + bytes memory _operatorSnapshotData, + bytes memory _VaultSnapshotData, + bytes memory _signature ) external { - require(block.timestamp >= lastCapturedTimestamp + SD, "Cooldown period not passed"); + require(block.timestamp >= lastCaptureTimestamp + SD, "Cooldown period not passed"); - require(length > 0, "Invalid length"); + require(_length > 0, "Invalid length"); - require(index < length, "Invalid index"); + require(_index < _length, "Invalid index"); - require(snapshotInfo[captureTimestamp][msg.sender].count > 0, "Snapshot fully submitted already"); - snapshotInfo[captureTimestamp][msg.sender].count--; + require(submissionInfo[_captureTimestamp][msg.sender].count > 0, "Snapshot fully submitted already"); + submissionInfo[_captureTimestamp][msg.sender].count--; - require(snapshotInfo[captureTimestamp][msg.sender].length == length, "Invalid length"); + require(submissionInfo[_captureTimestamp][msg.sender].length == _length, "Invalid length"); // TODO: Verify the signature // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image - // TODO: Data transmitter should get TC% of the rewards + OperatorSnapshot[] memory operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); + VaultSnapshot[] memory vaultSnapshots = abi.decode(_VaultSnapshotData, (VaultSnapshot[])); - // TODO: stakeData should be of the correct format which has key value pairs of operators and stakeDelta (?) + // TODO: loop through each snapshots and update the state - // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + // when the last chunk of the snapshot is received + if(submissionInfo[_captureTimestamp][msg.sender].count == 0) { + // TODO: update lastCaptureTimestamp - // TODO: Should update the latest complete snapshot information once the last chunk of staking snapshot is received (Updates TC based on the delay) + // TODO: calculate rewards for the transmitter based on TC + // TODO: Data transmitter should get TC% of the rewards + // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + } } + function _updateOperatorSnapshot(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { + for(uint256 i = 0; i < _operatorSnapshots.length; i++) { + } + } /*======================================== Getters ========================================*/ From e3a947a38d05e4524de7d9d416b945bc78cabfd5 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 30 Aug 2024 19:38:25 +0900 Subject: [PATCH 017/158] add mapping for slash result submission --- .../interfaces/staking/ISymbioticStaking.sol | 27 +++++++++ .../staking/l2_contracts/SymbioticStaking.sol | 56 ++++++++++--------- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index e69de29..5fe5cc7 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +interface ISymbioticStaking { + // function stakeOf(address _operator, address _token) external view returns (uint256); + + struct SnapshotTxInfo { + uint256 count; + uint256 length; + } + + struct OperatorSnapshot { + address operator; + uint256 stake; + } + + struct VaultSnapshot { + address vault; + uint256 stake; + } + + struct SlashResult { + uint256 jobId; + uint256 slashAmount; + address rewardAddress; + } +} diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index be0c6ca..ed39f58 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -contract SymbioticStaking { +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; + +contract SymbioticStaking is ISymbioticStaking{ // TODO: address Operator => address token => CheckPoints.Trace256 stakeAmount (Question: operators' stake amount is consolidated within same vault?) // TODO: set SD @@ -9,29 +11,16 @@ contract SymbioticStaking { // TODO: set TC - // TODO: lastCapturedTimestamp - uint256 lastCaptureTimestamp; - //? How to manage Vault lists? - struct SnapshotTxInfo { - uint256 count; - uint256 length; - } - - struct OperatorSnapshot { - address operator; - uint256 stake; - } - - struct VaultSnapshot { - address vault; - uint256 stake; - } - mapping(uint256 captureTimestamp => mapping(address account => SnapshotTxInfo snapshot)) submissionInfo; + mapping(uint256 captureTimestamp => mapping(address account => SnapshotTxInfo snapshot)) submissionInfo; // to check if all partial txs are received - // TODO: mappings for operator and vault snapshots + mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshot; + mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshot; + mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashREsult)) slashResults; // TODO: need to check slash timestamp? + + uint256[] public confirmedTimestamps; // Transmitter submits staking data snapshot // This should update StakingManger's state @@ -43,7 +32,7 @@ contract SymbioticStaking { bytes memory _VaultSnapshotData, bytes memory _signature ) external { - require(block.timestamp >= lastCaptureTimestamp + SD, "Cooldown period not passed"); + require(block.timestamp >= lastCaptureTimestamp() + SD, "Cooldown period not passed"); require(_length > 0, "Invalid length"); @@ -58,26 +47,43 @@ contract SymbioticStaking { // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image OperatorSnapshot[] memory operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); + _updateOperatorSnapshotInfo(_captureTimestamp, operatorSnapshots); + VaultSnapshot[] memory vaultSnapshots = abi.decode(_VaultSnapshotData, (VaultSnapshot[])); - - // TODO: loop through each snapshots and update the state + _updateVaultSnapshotInfo(_captureTimestamp, vaultSnapshots); // when the last chunk of the snapshot is received if(submissionInfo[_captureTimestamp][msg.sender].count == 0) { // TODO: update lastCaptureTimestamp + // TODO: calculate rewards for the transmitter based on TC // TODO: Data transmitter should get TC% of the rewards // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" } } - function _updateOperatorSnapshot(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { + function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { for(uint256 i = 0; i < _operatorSnapshots.length; i++) { + // TODO } } - /*======================================== Getters ========================================*/ + function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { + for(uint256 i = 0; i < _vaultSnapshots.length; i++) { + // TODO + } + } + function _updateSlashResultInfo(uint256 _captureTimestamp, address[] memory _operators, uint256[] memory _slashAmounts) internal { + for(uint256 i = 0; i < _operators.length; i++) { + // TODO + } + } + + /*======================================== Getters ========================================*/ + function lastCaptureTimestamp() public view returns(uint256) { + return confirmedTimestamps[confirmedTimestamps.length - 1]; + } } From 33c92e8713a94747e83098f1ded3bbca8297b7da Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 30 Aug 2024 19:38:54 +0900 Subject: [PATCH 018/158] fix return fix logic for getters --- .../staking/l2_contracts/NativeStaking.sol | 78 ++++++++++++++----- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index dd53996..b299e39 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -10,12 +10,8 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - - - import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; - contract NativeStaking is ContextUpgradeable, ERC165Upgradeable, @@ -31,10 +27,10 @@ contract NativeStaking is EnumerableSet.AddressSet private operatorSet; mapping(address operator => mapping(address token => OperatorStakeInfo stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount - mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public userStakeInfo; + mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public + userStakeInfo; mapping(bytes4 sig => bool isSupported) private supportedSignatures; - modifier onlySupportedToken(address _token) { require(tokenSet.contains(_token), "Token not supported"); @@ -46,13 +42,17 @@ contract NativeStaking is _; } - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC165Upgradeable, AccessControlUpgradeable) returns (bool) { + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { return super.supportsInterface(interfaceId); } - function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} function initialize(address _admin) public initializer { __Context_init_unchained(); @@ -65,7 +65,12 @@ contract NativeStaking is // Staker should be able to choose an Operator they want to stake into // This should update StakingManger's state - function stake(address _account, address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { + function stake(address _account, address _operator, address _token, uint256 _amount) + external + onlySupportedSignature(msg.sig) + onlySupportedToken(_token) + nonReentrant + { IERC20(_token).safeTransferFrom(_account, address(this), _amount); userStakeInfo[_account][_operator][_token] += _amount; @@ -76,7 +81,12 @@ contract NativeStaking is // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) // This should update StakingManger's state - function operatorSelfStake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { + function operatorSelfStake(address _operator, address _token, uint256 _amount) + external + onlySupportedSignature(msg.sig) + onlySupportedToken(_token) + nonReentrant + { IERC20(_token).safeTransferFrom(_operator, address(this), _amount); operatorStakeInfo[_operator][_token].selfStake += _amount; @@ -112,12 +122,21 @@ contract NativeStaking is /*======================================== Getters ========================================*/ - function getDelegatedStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + function getDelegatedStake(address _operator, address _token) + external + view + onlySupportedToken(_token) + returns (uint256) + { return operatorStakeInfo[_operator][_token].delegatedStake; } // Returns the list of tokenSet staked by the operator and the amounts - function getDelegatedStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + function getDelegatedStakes(address _operator) + external + view + returns (address[] memory _tokens, uint256[] memory _amounts) + { uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { @@ -126,11 +145,20 @@ contract NativeStaking is } } - function getSelfStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + function getSelfStake(address _operator, address _token) + external + view + onlySupportedToken(_token) + returns (uint256) + { return operatorStakeInfo[_operator][_token].selfStake; } - function getSelfStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + function getSelfStakes(address _operator) + external + view + returns (address[] memory _tokens, uint256[] memory _amounts) + { uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { @@ -139,12 +167,21 @@ contract NativeStaking is } } - function getOperatorTotalStake(address _operator, address _token) external view onlySupportedToken(_token) returns (uint256) { + function getOperatorTotalStake(address _operator, address _token) + external + view + onlySupportedToken(_token) + returns (uint256) + { OperatorStakeInfo memory _stakeInfo = operatorStakeInfo[_operator][_token]; return _stakeInfo.delegatedStake + _stakeInfo.selfStake; } - function getOperatorTotalStakes(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts) { + function getOperatorTotalStakes(address _operator) + external + view + returns (address[] memory _tokens, uint256[] memory _amounts) + { uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { @@ -159,7 +196,8 @@ contract NativeStaking is uint256 totalStake; for (uint256 i = 0; i < len; i++) { - totalStake += operatorStakeInfo[operatorSet.at(i)][_token].delegatedStake + operatorStakeInfo[operatorSet.at(i)][_token].selfStake; + totalStake += operatorStakeInfo[operatorSet.at(i)][_token].delegatedStake + + operatorStakeInfo[operatorSet.at(i)][_token].selfStake; } return totalStake; @@ -178,7 +216,7 @@ contract NativeStaking is function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { require(tokenSet.remove(token), "Token does not exist"); } - + function setSupportedSignature(bytes4 sig, bool isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { supportedSignatures[sig] = isSupported; } From 335deeb30971b698cd0273f102a99c92a2b7dbba Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 30 Aug 2024 22:42:25 +0900 Subject: [PATCH 019/158] add Snapshot submission logic --- .../interfaces/staking/ISymbioticStaking.sol | 10 ++- .../staking/l2_contracts/SymbioticStaking.sol | 79 ++++++++++++++----- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 5fe5cc7..243388f 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; interface ISymbioticStaking { // function stakeOf(address _operator, address _token) external view returns (uint256); - struct SnapshotTxInfo { + struct SnapshotTxCountInfo { uint256 count; uint256 length; } @@ -24,4 +24,12 @@ interface ISymbioticStaking { uint256 slashAmount; address rewardAddress; } + + // event OperatorSnapshotSubmitted + + // event VaultSnapshotSubmitted + + // event SlashResultSubmitted + + // event SubmissionCompleted } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index ed39f58..e3d3fb2 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -13,54 +13,93 @@ contract SymbioticStaking is ISymbioticStaking{ //? How to manage Vault lists? + bytes4 public constant OPERATOR_SNAPSHOT_MASK = 0x00000001; + bytes4 public constant VAULT_SNAPSHOT_MASK = 0x00000010; + bytes4 public constant SLASH_RESULT_MASK = 0x00000100; + bytes4 public constant COMPLETE_MASK = 0x00000111; - mapping(uint256 captureTimestamp => mapping(address account => SnapshotTxInfo snapshot)) submissionInfo; // to check if all partial txs are received + bytes32 public constant OPERATOR_SNAPSHOT = keccak256("OPERATOR_SNAPSHOT"); + bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); + bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); + + + mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received + mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshot; mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshot; - mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashREsult)) slashResults; // TODO: need to check slash timestamp? + mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashResult)) slashResults; // TODO: need to check actual slashing timestamp? - uint256[] public confirmedTimestamps; + uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received // Transmitter submits staking data snapshot // This should update StakingManger's state - function submitSnapshot( + function submitOperatorSnapshot( uint256 _index, - uint256 _length, // number of total transactions + uint256 _numOfTxs, // number of total transactions uint256 _captureTimestamp, bytes memory _operatorSnapshotData, - bytes memory _VaultSnapshotData, bytes memory _signature ) external { require(block.timestamp >= lastCaptureTimestamp() + SD, "Cooldown period not passed"); - require(_length > 0, "Invalid length"); - - require(_index < _length, "Invalid index"); + require(_numOfTxs > 0, "Invalid length"); - require(submissionInfo[_captureTimestamp][msg.sender].count > 0, "Snapshot fully submitted already"); - submissionInfo[_captureTimestamp][msg.sender].count--; + require(_index < _numOfTxs, "Invalid index"); - require(submissionInfo[_captureTimestamp][msg.sender].length == _length, "Invalid length"); + + SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); + require(snapshot.length == _numOfTxs, "Invalid length"); + require(submissionStatus[_captureTimestamp][msg.sender] & OPERATOR_SNAPSHOT_MASK == 0, "Snapshot fully submitted already"); // TODO: Verify the signature // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + // main update logic OperatorSnapshot[] memory operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); _updateOperatorSnapshotInfo(_captureTimestamp, operatorSnapshots); - VaultSnapshot[] memory vaultSnapshots = abi.decode(_VaultSnapshotData, (VaultSnapshot[])); - _updateVaultSnapshotInfo(_captureTimestamp, vaultSnapshots); + // increase count by 1 + snapshot.count += 1; - // when the last chunk of the snapshot is received - if(submissionInfo[_captureTimestamp][msg.sender].count == 0) { - // TODO: update lastCaptureTimestamp + // update length if 0 + if(snapshot.length == 0) { + snapshot.length = _numOfTxs; + } + // when all chunks of OperatorSnapshot are submitted + if(snapshot.count == snapshot.length) { + submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; + } - // TODO: calculate rewards for the transmitter based on TC - // TODO: Data transmitter should get TC% of the rewards - // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + if(_isCompleteStatus(_captureTimestamp)) { + _completeSubmission(_captureTimestamp); } + + } + + function submitVaultSnapshot() external { + // TODO + } + + function submitSlashResult() external { + // TODO + } + + /*======================================== Helpers ========================================*/ + + + function _isCompleteStatus(uint256 _captureTimestamp) internal view returns(bool) { + return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; + } + + function _completeSubmission(uint256 _captureTimestamp) internal { + confirmedTimestamps.push(_captureTimestamp); + + // TODO: calculate rewards for the transmitter based on TC + // TODO: Data transmitter should get TC% of the rewards + // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" } function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { From 7193efb68206d309ebad2c3e6cae3ca122675d8c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 30 Aug 2024 23:33:48 +0900 Subject: [PATCH 020/158] add operator snapshot update logic --- contracts/interfaces/staking/ISymbioticStaking.sol | 1 + .../staking/l2_contracts/SymbioticStaking.sol | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 243388f..a7340f5 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -11,6 +11,7 @@ interface ISymbioticStaking { struct OperatorSnapshot { address operator; + address token; uint256 stake; } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index e3d3fb2..f3fd725 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -26,8 +26,8 @@ contract SymbioticStaking is ISymbioticStaking{ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received - mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshot; - mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshot; + mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshots; + mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshots; mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashResult)) slashResults; // TODO: need to check actual slashing timestamp? uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received @@ -57,8 +57,8 @@ contract SymbioticStaking is ISymbioticStaking{ // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image // main update logic - OperatorSnapshot[] memory operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); - _updateOperatorSnapshotInfo(_captureTimestamp, operatorSnapshots); + OperatorSnapshot[] memory _operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); + _updateOperatorSnapshotInfo(_captureTimestamp, _operatorSnapshots); // increase count by 1 snapshot.count += 1; @@ -75,6 +75,7 @@ contract SymbioticStaking is ISymbioticStaking{ if(_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); + // TODO: emit SubmissionCompleted } } @@ -100,11 +101,14 @@ contract SymbioticStaking is ISymbioticStaking{ // TODO: calculate rewards for the transmitter based on TC // TODO: Data transmitter should get TC% of the rewards // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + } function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { for(uint256 i = 0; i < _operatorSnapshots.length; i++) { - // TODO + OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; + + operatorSnapshots[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = _operatorSnapshot.stake; } } From 12912046d53b36fbe4bd21efa39f4cb8b0524b8d Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 30 Aug 2024 23:48:02 +0900 Subject: [PATCH 021/158] add vault snapshot logic --- .../interfaces/staking/ISymbioticStaking.sol | 1 + .../staking/l2_contracts/SymbioticStaking.sol | 58 ++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index a7340f5..ab93efc 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -17,6 +17,7 @@ interface ISymbioticStaking { struct VaultSnapshot { address vault; + address token; uint256 stake; } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index f3fd725..a364f76 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -41,16 +41,16 @@ contract SymbioticStaking is ISymbioticStaking{ bytes memory _operatorSnapshotData, bytes memory _signature ) external { - require(block.timestamp >= lastCaptureTimestamp() + SD, "Cooldown period not passed"); + require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); require(_numOfTxs > 0, "Invalid length"); - require(_index < _numOfTxs, "Invalid index"); - SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); require(snapshot.length == _numOfTxs, "Invalid length"); + require(submissionStatus[_captureTimestamp][msg.sender] & OPERATOR_SNAPSHOT_MASK == 0, "Snapshot fully submitted already"); // TODO: Verify the signature @@ -77,7 +77,51 @@ contract SymbioticStaking is ISymbioticStaking{ _completeSubmission(_captureTimestamp); // TODO: emit SubmissionCompleted } + } + + function submitVaultSnapshot( + uint256 _index, + uint256 _numOfTxs, // number of total transactions + uint256 _captureTimestamp, + bytes memory _vaultSnapshotData, + bytes memory _signature + ) external { + require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); + require(_numOfTxs > 0, "Invalid length"); + require(_index < _numOfTxs, "Invalid index"); + + SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; + + require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); + require(snapshot.length == _numOfTxs, "Invalid length"); + + require(submissionStatus[_captureTimestamp][msg.sender] & VAULT_SNAPSHOT_MASK == 0, "Snapshot fully submitted already"); + + // TODO: Verify the signature + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + + // main update logic + VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (VaultSnapshot[])); + _updateVaultSnapshotInfo(_captureTimestamp, _vaultSnapshots); + + // increase count by 1 + snapshot.count += 1; + + // update length if 0 + if(snapshot.length == 0) { + snapshot.length = _numOfTxs; + } + + // when all chunks of OperatorSnapshot are submitted + if(snapshot.count == snapshot.length) { + submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; + } + + if(_isCompleteStatus(_captureTimestamp)) { + _completeSubmission(_captureTimestamp); + // TODO: emit SubmissionCompleted + } } function submitVaultSnapshot() external { @@ -89,8 +133,6 @@ contract SymbioticStaking is ISymbioticStaking{ } /*======================================== Helpers ========================================*/ - - function _isCompleteStatus(uint256 _captureTimestamp) internal view returns(bool) { return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; } @@ -114,7 +156,9 @@ contract SymbioticStaking is ISymbioticStaking{ function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { for(uint256 i = 0; i < _vaultSnapshots.length; i++) { - // TODO + VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; + + vaultSnapshots[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; } } @@ -125,7 +169,7 @@ contract SymbioticStaking is ISymbioticStaking{ } /*======================================== Getters ========================================*/ - function lastCaptureTimestamp() public view returns(uint256) { + function lastConfirmedTimestamp() public view returns(uint256) { return confirmedTimestamps[confirmedTimestamps.length - 1]; } From 98499ee5ed453030eb42444f4c044c32548ae114 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 31 Aug 2024 00:17:43 +0900 Subject: [PATCH 022/158] add slash result submission logic --- .../interfaces/staking/ISymbioticStaking.sol | 6 ++- .../staking/l2_contracts/SymbioticStaking.sol | 47 +++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index ab93efc..dea6d71 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -21,8 +21,12 @@ interface ISymbioticStaking { uint256 stake; } - struct SlashResult { + struct SlashResultData { uint256 jobId; + SlashResult slashResult; + } + + struct SlashResult { uint256 slashAmount; address rewardAddress; } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index a364f76..5862565 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -10,6 +10,7 @@ contract SymbioticStaking is ISymbioticStaking{ uint256 SD; // TODO: set TC + uint256 TC; //? How to manage Vault lists? @@ -28,7 +29,7 @@ contract SymbioticStaking is ISymbioticStaking{ mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshots; mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshots; - mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashResult)) slashResults; // TODO: need to check actual slashing timestamp? + mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) SlashResultDatas; // TODO: need to check actual slashing timestamp? uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received @@ -124,12 +125,27 @@ contract SymbioticStaking is ISymbioticStaking{ } } - function submitVaultSnapshot() external { - // TODO - } + function submitSlashResultData( + uint256 _index, + uint256 _numOfTxs, // number of total transactions + uint256 _captureTimestamp, + bytes memory _SlashResultDataData, + bytes memory _signature + ) external { + require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); - function submitSlashResult() external { - // TODO + require(_numOfTxs > 0, "Invalid length"); + require(_index < _numOfTxs, "Invalid index"); + + SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][SLASH_RESULT]; + + require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); + require(snapshot.length == _numOfTxs, "Invalid length"); + + require(submissionStatus[_captureTimestamp][msg.sender] & SLASH_RESULT_MASK == 0, "Snapshot fully submitted already"); + + // TODO: Verify the signature + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image } /*======================================== Helpers ========================================*/ @@ -151,6 +167,8 @@ contract SymbioticStaking is ISymbioticStaking{ OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; operatorSnapshots[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = _operatorSnapshot.stake; + + // TODO: emit event for each update? } } @@ -159,13 +177,24 @@ contract SymbioticStaking is ISymbioticStaking{ VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; vaultSnapshots[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; + + // TODO: emit event for each update? } } - function _updateSlashResultInfo(uint256 _captureTimestamp, address[] memory _operators, uint256[] memory _slashAmounts) internal { - for(uint256 i = 0; i < _operators.length; i++) { - // TODO + function _updateSlashResultDataInfo(uint256 _captureTimestamp, SlashResultData[] memory _SlashResultDatas) internal { + for(uint256 i = 0; i < _SlashResultDatas.length; i++) { + SlashResultData memory _slashResultData = _SlashResultDatas[i]; + + SlashResultDatas[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; + + // TODO: emit event for each update? } + + } + + function _verifySignature(bytes memory _data, bytes memory _signature) internal { + // TODO } /*======================================== Getters ========================================*/ From b27093d3dac8636299d4c067f3338f4d6a1d1640 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 31 Aug 2024 00:41:42 +0900 Subject: [PATCH 023/158] refactor code --- .../interfaces/staking/ISymbioticStaking.sol | 4 +- .../staking/l2_contracts/SymbioticStaking.sol | 158 +++++++++--------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index dea6d71..d62d38e 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -6,7 +6,7 @@ interface ISymbioticStaking { struct SnapshotTxCountInfo { uint256 count; - uint256 length; + uint256 numOfTxs; } struct OperatorSnapshot { @@ -25,7 +25,7 @@ interface ISymbioticStaking { uint256 jobId; SlashResult slashResult; } - + struct SlashResult { uint256 slashAmount; address rewardAddress; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 5862565..cc1745f 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; -contract SymbioticStaking is ISymbioticStaking{ +contract SymbioticStaking is ISymbioticStaking { // TODO: address Operator => address token => CheckPoints.Trace256 stakeAmount (Question: operators' stake amount is consolidated within same vault?) // TODO: set SD @@ -23,16 +23,15 @@ contract SymbioticStaking is ISymbioticStaking{ bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); - mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received - - mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshots; - mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshots; + + mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))operatorSnapshots; + mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))vaultSnapshots; mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) SlashResultDatas; // TODO: need to check actual slashing timestamp? uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received - + // Transmitter submits staking data snapshot // This should update StakingManger's state function submitOperatorSnapshot( @@ -42,39 +41,24 @@ contract SymbioticStaking is ISymbioticStaking{ bytes memory _operatorSnapshotData, bytes memory _signature ) external { - require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); - - require(_numOfTxs > 0, "Invalid length"); - require(_index < _numOfTxs, "Invalid index"); - - SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; - - require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); - require(snapshot.length == _numOfTxs, "Invalid length"); + _checkValidity(_index, _numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); - require(submissionStatus[_captureTimestamp][msg.sender] & OPERATOR_SNAPSHOT_MASK == 0, "Snapshot fully submitted already"); - - // TODO: Verify the signature - // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + _verifySignature(_index, _numOfTxs, _captureTimestamp, _operatorSnapshotData, _signature); // main update logic OperatorSnapshot[] memory _operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); _updateOperatorSnapshotInfo(_captureTimestamp, _operatorSnapshots); - - // increase count by 1 - snapshot.count += 1; - // update length if 0 - if(snapshot.length == 0) { - snapshot.length = _numOfTxs; - } + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + + _updateTxCountInfo(_index, _numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); // when all chunks of OperatorSnapshot are submitted - if(snapshot.count == snapshot.length) { + if (_snapshot.count == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; } - if(_isCompleteStatus(_captureTimestamp)) { + if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); // TODO: emit SubmissionCompleted } @@ -87,39 +71,24 @@ contract SymbioticStaking is ISymbioticStaking{ bytes memory _vaultSnapshotData, bytes memory _signature ) external { - require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); + _checkValidity(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); - require(_numOfTxs > 0, "Invalid length"); - require(_index < _numOfTxs, "Invalid index"); - - SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; - - require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); - require(snapshot.length == _numOfTxs, "Invalid length"); - - require(submissionStatus[_captureTimestamp][msg.sender] & VAULT_SNAPSHOT_MASK == 0, "Snapshot fully submitted already"); - - // TODO: Verify the signature - // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); // main update logic VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (VaultSnapshot[])); _updateVaultSnapshotInfo(_captureTimestamp, _vaultSnapshots); - - // increase count by 1 - snapshot.count += 1; - // update length if 0 - if(snapshot.length == 0) { - snapshot.length = _numOfTxs; - } + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + + _updateTxCountInfo(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); // when all chunks of OperatorSnapshot are submitted - if(snapshot.count == snapshot.length) { + if (_snapshot.count == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; } - if(_isCompleteStatus(_captureTimestamp)) { + if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); // TODO: emit SubmissionCompleted } @@ -132,48 +101,71 @@ contract SymbioticStaking is ISymbioticStaking{ bytes memory _SlashResultDataData, bytes memory _signature ) external { - require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); - - require(_numOfTxs > 0, "Invalid length"); - require(_index < _numOfTxs, "Invalid index"); - - SnapshotTxCountInfo storage snapshot = txCountInfo[_captureTimestamp][msg.sender][SLASH_RESULT]; - - require(snapshot.count < snapshot.length, "Snapshot fully submitted already"); - require(snapshot.length == _numOfTxs, "Invalid length"); + _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT); - require(submissionStatus[_captureTimestamp][msg.sender] & SLASH_RESULT_MASK == 0, "Snapshot fully submitted already"); + _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultDataData, _signature); // TODO: Verify the signature // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image } /*======================================== Helpers ========================================*/ - function _isCompleteStatus(uint256 _captureTimestamp) internal view returns(bool) { - return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; + function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { + require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); + + require(_numOfTxs > 0, "Invalid length"); + require(_index < _numOfTxs, "Invalid index"); + + SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; + require(snapshot.count < snapshot.numOfTxs, "Snapshot fully submitted already"); + require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); + + bytes4 mask; + if (_type == OPERATOR_SNAPSHOT) mask = OPERATOR_SNAPSHOT_MASK; + else if (_type == VAULT_SNAPSHOT) mask = VAULT_SNAPSHOT_MASK; + else if (_type == SLASH_RESULT) mask = SLASH_RESULT_MASK; + + require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Snapshot fully submitted already"); } - function _completeSubmission(uint256 _captureTimestamp) internal { - confirmedTimestamps.push(_captureTimestamp); + function _updateTxCountInfo(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; - // TODO: calculate rewards for the transmitter based on TC - // TODO: Data transmitter should get TC% of the rewards - // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + // increase count by 1 + txCountInfo[_captureTimestamp][msg.sender][_type].count += 1; + + // update length if 0 + if (_snapshot.numOfTxs == 0) { + txCountInfo[_captureTimestamp][msg.sender][_type].numOfTxs = _numOfTxs; + } + + } + function _verifySignature(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes memory _data, bytes memory _signature) internal { + // TODO: Verify the signature + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + } + + function _isCompleteStatus(uint256 _captureTimestamp) internal view returns (bool) { + return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; } - function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) internal { - for(uint256 i = 0; i < _operatorSnapshots.length; i++) { + function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) + internal + { + for (uint256 i = 0; i < _operatorSnapshots.length; i++) { OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; - operatorSnapshots[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = _operatorSnapshot.stake; + operatorSnapshots[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = + _operatorSnapshot.stake; // TODO: emit event for each update? } } + function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { - for(uint256 i = 0; i < _vaultSnapshots.length; i++) { + for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; vaultSnapshots[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; @@ -182,24 +174,32 @@ contract SymbioticStaking is ISymbioticStaking{ } } - function _updateSlashResultDataInfo(uint256 _captureTimestamp, SlashResultData[] memory _SlashResultDatas) internal { - for(uint256 i = 0; i < _SlashResultDatas.length; i++) { + function _updateSlashResultDataInfo(uint256 _captureTimestamp, SlashResultData[] memory _SlashResultDatas) + internal + { + for (uint256 i = 0; i < _SlashResultDatas.length; i++) { SlashResultData memory _slashResultData = _SlashResultDatas[i]; SlashResultDatas[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; - + // TODO: emit event for each update? } - } - function _verifySignature(bytes memory _data, bytes memory _signature) internal { - // TODO + function _completeSubmission(uint256 _captureTimestamp) internal { + confirmedTimestamps.push(_captureTimestamp); + + // TODO: calculate rewards for the transmitter based on TC + // TODO: Data transmitter should get TC% of the rewards + // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" } + + + + /*======================================== Getters ========================================*/ - function lastConfirmedTimestamp() public view returns(uint256) { + function lastConfirmedTimestamp() public view returns (uint256) { return confirmedTimestamps[confirmedTimestamps.length - 1]; } - } From 4d8461e330036e037e4b4cb8c646b845ffcf8836 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 31 Aug 2024 00:45:58 +0900 Subject: [PATCH 024/158] finish submitSlashResultData logic --- .../staking/l2_contracts/SymbioticStaking.sol | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index cc1745f..c22fac2 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -51,7 +51,7 @@ contract SymbioticStaking is ISymbioticStaking { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; - _updateTxCountInfo(_index, _numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); + _updateTxCountInfo(_numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); // when all chunks of OperatorSnapshot are submitted if (_snapshot.count == _snapshot.numOfTxs) { @@ -60,7 +60,6 @@ contract SymbioticStaking is ISymbioticStaking { if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); - // TODO: emit SubmissionCompleted } } @@ -79,10 +78,9 @@ contract SymbioticStaking is ISymbioticStaking { VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (VaultSnapshot[])); _updateVaultSnapshotInfo(_captureTimestamp, _vaultSnapshots); - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; - - _updateTxCountInfo(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); + _updateTxCountInfo(_numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.count == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; @@ -90,7 +88,6 @@ contract SymbioticStaking is ISymbioticStaking { if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); - // TODO: emit SubmissionCompleted } } @@ -105,12 +102,24 @@ contract SymbioticStaking is ISymbioticStaking { _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultDataData, _signature); - // TODO: Verify the signature - // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + SlashResultData[] memory _SlashResultDatas = abi.decode(_SlashResultDataData, (SlashResultData[])); + _updateSlashResultDataInfo(_captureTimestamp, _SlashResultDatas); + + _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); + + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + // when all chunks of OperatorSnapshot are submitted + if (_snapshot.count == _snapshot.numOfTxs) { + submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; + } + + if (_isCompleteStatus(_captureTimestamp)) { + _completeSubmission(_captureTimestamp); + } } /*======================================== Helpers ========================================*/ - function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { + function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); require(_numOfTxs > 0, "Invalid length"); @@ -128,7 +137,7 @@ contract SymbioticStaking is ISymbioticStaking { require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Snapshot fully submitted already"); } - function _updateTxCountInfo(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { + function _updateTxCountInfo(uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; // increase count by 1 @@ -192,11 +201,9 @@ contract SymbioticStaking is ISymbioticStaking { // TODO: calculate rewards for the transmitter based on TC // TODO: Data transmitter should get TC% of the rewards // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" - } - - - + // TODO: emit SubmissionCompleted + } /*======================================== Getters ========================================*/ function lastConfirmedTimestamp() public view returns (uint256) { From 42a6c0237dcdcfa4da0522db46949a3f071293f8 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 31 Aug 2024 00:46:29 +0900 Subject: [PATCH 025/158] fix typo --- contracts/staking/l2_contracts/SymbioticStaking.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index c22fac2..ffeda13 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -91,18 +91,18 @@ contract SymbioticStaking is ISymbioticStaking { } } - function submitSlashResultData( + function submitSlashResult( uint256 _index, uint256 _numOfTxs, // number of total transactions uint256 _captureTimestamp, - bytes memory _SlashResultDataData, + bytes memory _SlashResultData, bytes memory _signature ) external { _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT); - _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultDataData, _signature); + _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultData, _signature); - SlashResultData[] memory _SlashResultDatas = abi.decode(_SlashResultDataData, (SlashResultData[])); + SlashResultData[] memory _SlashResultDatas = abi.decode(_SlashResultData, (SlashResultData[])); _updateSlashResultDataInfo(_captureTimestamp, _SlashResultDatas); _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); From 933809d3e79218b7726b6b0ae9860e02621c0d9c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 31 Aug 2024 00:50:27 +0900 Subject: [PATCH 026/158] fix typo --- contracts/staking/l2_contracts/SymbioticStaking.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index ffeda13..fc76713 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -28,7 +28,7 @@ contract SymbioticStaking is ISymbioticStaking { mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))operatorSnapshots; mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))vaultSnapshots; - mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) SlashResultDatas; // TODO: need to check actual slashing timestamp? + mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) slashResultDatas; // TODO: need to check actual slashing timestamp? uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received @@ -189,7 +189,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _SlashResultDatas.length; i++) { SlashResultData memory _slashResultData = _SlashResultDatas[i]; - SlashResultDatas[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; + slashResultDatas[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; // TODO: emit event for each update? } From 0a277b77a03acc78eb85dd9bd696e6db9aa2761c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sun, 1 Sep 2024 00:42:27 +0900 Subject: [PATCH 027/158] add upgradeability to StakingManager --- .../staking/l2_contracts/StakingManager.sol | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index b82a123..6d7950a 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -1,12 +1,36 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; - -contract StakingManager { +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; + +contract StakingManager is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + INativeStaking { // TODO: Staking Pool Set // TODO: Staking Pool flag // TODO: integration with Slasher + function initialize(address _admin) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } // TODO: Self stake is given to the slasher while the rest of the stakers is burnt when slashed // TODO: check necessary params @@ -92,4 +116,18 @@ contract StakingManager { function setPriceOracle(address _priceOracle) external { // TODO } + + /*======================================== Override ========================================*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} } \ No newline at end of file From 40e0b0154f1650163f4c6d0a4753d042fec869b3 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 2 Sep 2024 00:52:14 +0900 Subject: [PATCH 028/158] add logic for jobCreation and stake lock --- .../interfaces/staking/IKalypsoStaking.sol | 8 +- .../staking/l2_contracts/StakingManager.sol | 123 ++++++++++++++---- 2 files changed, 102 insertions(+), 29 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index fa2cd95..fbfcb5b 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -1,6 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -interface IKalypsoStaking { +interface IKalypsoStakingPool { // function stakeOf(address _operator, address _token) external view returns (uint256); + + function isSupportedToken(address _token) external view returns (bool); + + function getStakeAmount(address _operator, address _token) external view returns (uint256); + + function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts); } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 6d7950a..fbfeb61 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; + import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; @@ -17,9 +18,34 @@ contract StakingManager is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable, - INativeStaking { + INativeStaking +{ + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + // TODO: Staking Pool Set - // TODO: Staking Pool flag + EnumerableSet.AddressSet private stakingPoolSet; + + // TODO: Staking Pool flag + // mapping(address pool => bool isEnabled) private stakingPoolStatus; + // mapping(address pool => uint256 weight) private stakingPoolWeight; + + mapping(address pool => PoolConfig config) private poolConfig; + mapping(uint256 jobId => mapping(address pool => LockInfo lockInfo)) private lockInfo; + + uint256 unlockEpoch; + + struct PoolConfig { + uint256 weight; + uint256 minStake; + bool isEnabled; + } + + // operator, lockToken, lockAmount should be stored in JobManager + struct LockInfo { + uint256 lockAmount; // locked amount for the pool + uint256 unlockTimestamp; + } // TODO: integration with Slasher @@ -34,29 +60,67 @@ contract StakingManager is // TODO: Self stake is given to the slasher while the rest of the stakers is burnt when slashed // TODO: check necessary params - function slashJob(address _jobId, address _vault, uint256 _captureTimestamp, uint256 _amount, address _rewardAddress) external { + function slashJob( + address _jobId, + address _vault, + uint256 _captureTimestamp, + uint256 _amount, + address _rewardAddress + ) external { // TODO: only slashingManager } // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) // locked stake will be unlocked after an epoch if no slas result is submitted - function onJobCreation(address _jobId, address _operator, uint256 _amount) external { + + // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) + function onJobCreation(address _jobId, address _operator, address token, uint256 _lockAmount) external { // TODO: only jobManager + + // TODO: lock operator selfstake + + address[] memory enabledPoolList = getEnabledPoolList(); + + uint256 len = enabledPoolList.length; + for(uint256 i = 0; i < len; i++) { + pool = enabledPoolList[i]; + + // skip if the token is not supported by the pool + if(!IKalypsoStakingPool(pool).isSupportedToken(token)) continue; + + uint256 poolStake = getPoolStake(pool, _operator, token); + uint256 minStake = poolConfig[pool].minStake; + + // skip if the pool stake is less than the minStake + if(poolStake >= minStake) { + // lock the stake + uint256 lockAmount = _calcLockAmount(poolStake, poolConfig[pool].weight); // TODO: need to check formula for calculation + _lockStake(pool, lockAmount); + } + } + } + + function _lockStake(address _jobId, uint256 amount) internal { + lockInfo[_jobId].lockAmount += amount; + lockInfo[_jobId].unlockTimestamp = block.timestamp + unlockEpoch; + } + + function _unlockStake(address _jobId) internal { + lockInfo[_jobId].lockAmount = 0; + lockInfo[_jobId].unlockTimestamp = 0; } // called when job is completed to unlock the locked stakes function onJobCompletion(address _jobId) external { // TODO: only jobManager - } - // called when Staked/Unstaked in the Staking Pool (Native Staking, Symbiotic Staking) - function updateStake(address operator, address token, uint256 amount) external { - // TODO: only Staking Pool + // TODO: unlock the locked stakes } // when certain period has passed after the lock and no slash result is submitted, this can be unlocked - function unlockStake(address _jobId) external { } - + function unlockStake(address _jobId) external { + + } /*======================================== Getters ========================================*/ @@ -64,29 +128,28 @@ contract StakingManager is // this only tells if the deadline for proof submission has passed // so even when this function returns true and transaction submitted to L1 can be reverted // when someone already has submitted the proof - function isSlashable(address _jobId) external view returns(bool) { - // TODO + function isSlashable(address _jobId) external view returns (bool) { + // TODO: check if the proof was submitted before the deadline } - // for all operators - function getLatestStakeInfo() public { - // TODO - } + /*======================================== Getter for Staking ========================================*/ - function getLatestStakeInfoAt(uint256 timestamp) public { - // TODO + function _calcLockAmount(uint256 amount, uint256 weight) internal pure returns (uint256) { + return (amount * weight) / 10000; // TODO: need to check formula for calculation (probably be the share) } - // for a specific operator - function getLatestStakeInfo(address operator) public { - // TODO - } - function getStakeInfoAt(address operator, address token, uint256 timestamp) public returns(uint256) { - // TODO + function getEnabledPoolList() public view returns (address[] memory enabledPoolList) { + uint256 len = stakingPoolSet.length; + for(uint256 i = 0; i < len; i++) { + pool = stakingPoolSet.at(i); + + if(poolConfig[pool].isEnabled) { + enabledPoolList.push(pool); + } + } } - // TODO: function that consolidates the staking information from both Native Staking and Symbiotic Staking and returns the total stake amount /*======================================== Admin ========================================*/ @@ -97,7 +160,7 @@ contract StakingManager is function removeStakingPool(address _stakingPool) external { // TODO: onlyAdmin - } + } function setStakingPoolStatus(address _stakingPool, bool _status) external { // TODO: onlyAdmin @@ -117,8 +180,12 @@ contract StakingManager is // TODO } + function setUnlockEpoch(uint256 _unlockEpoch) external { + // TODO: check if the unlockEpoch is longer than the proofDeadline + } + /*======================================== Override ========================================*/ - + function supportsInterface(bytes4 interfaceId) public view @@ -130,4 +197,4 @@ contract StakingManager is } function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} -} \ No newline at end of file +} From 2463e2cc694706a1d2cdd3cca66b4b2b589dbf5a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 2 Sep 2024 20:58:26 +0900 Subject: [PATCH 029/158] implement unlock stake --- .../staking/l2_contracts/StakingManager.sol | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index fbfeb61..7d303ca 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -77,7 +77,7 @@ contract StakingManager is function onJobCreation(address _jobId, address _operator, address token, uint256 _lockAmount) external { // TODO: only jobManager - // TODO: lock operator selfstake + // TODO: lock operator selfstake (check how much) address[] memory enabledPoolList = getEnabledPoolList(); @@ -95,11 +95,14 @@ contract StakingManager is if(poolStake >= minStake) { // lock the stake uint256 lockAmount = _calcLockAmount(poolStake, poolConfig[pool].weight); // TODO: need to check formula for calculation + // TODO: move fund from the pool (implement lockStake in each pool) + // TODO: SymbioticStaking will just have empty code in it _lockStake(pool, lockAmount); } } } - + + // TODO: make sure nothing happens when storing value only function _lockStake(address _jobId, uint256 amount) internal { lockInfo[_jobId].lockAmount += amount; lockInfo[_jobId].unlockTimestamp = block.timestamp + unlockEpoch; @@ -115,11 +118,22 @@ contract StakingManager is // TODO: only jobManager // TODO: unlock the locked stakes + _unlockStake(_jobId); } // when certain period has passed after the lock and no slash result is submitted, this can be unlocked + // unlocking the locked stake does not check if token is enabled function unlockStake(address _jobId) external { - + // TODO: query if deadline has passed + no proof submission + not slashed + + uint256 len = stakingPoolSet.length; + + for(uint256 i = 0; i < len; i++) { + pool = stakingPoolSet.at(i); + + // unlock the stake + _unlockStake(pool, _jobId); + } } /*======================================== Getters ========================================*/ @@ -129,7 +143,7 @@ contract StakingManager is // so even when this function returns true and transaction submitted to L1 can be reverted // when someone already has submitted the proof function isSlashable(address _jobId) external view returns (bool) { - // TODO: check if the proof was submitted before the deadline + // TODO: check if the proof was submitted before the deadline, so need to query jobmanager } /*======================================== Getter for Staking ========================================*/ From 1d90243b7bf2d68a51ee87662e28f8bfa00c6bc1 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 3 Sep 2024 12:25:28 +0900 Subject: [PATCH 030/158] fix compile erorrs --- .../interfaces/staking/IKalypsoStaking.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 6 ++ .../staking/l2_contracts/StakingManager.sol | 70 +++++++++---------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index fbfcb5b..f22d7fd 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -interface IKalypsoStakingPool { +interface IKalypsoStaking { // function stakeOf(address _operator, address _token) external view returns (uint256); function isSupportedToken(address _token) external view returns (bool); diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index b299e39..ba85f2b 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -122,6 +122,12 @@ contract NativeStaking is /*======================================== Getters ========================================*/ + function getStakeAmount(address _operator, address _token) external view returns (uint256) {} + + function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts) {} + + function isSupportedToken(address _token) external view returns (bool) {} + function getDelegatedStake(address _operator, address _token) external view diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 7d303ca..9cbdd09 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -11,14 +11,15 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; +import {IKalypsoStaking} from "../../interfaces/staking/IKalypsoStaking.sol"; contract StakingManager is ContextUpgradeable, ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, - ReentrancyGuardUpgradeable, - INativeStaking + ReentrancyGuardUpgradeable + // INativeStaking { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -31,19 +32,21 @@ contract StakingManager is // mapping(address pool => uint256 weight) private stakingPoolWeight; mapping(address pool => PoolConfig config) private poolConfig; - mapping(uint256 jobId => mapping(address pool => LockInfo lockInfo)) private lockInfo; + + mapping(uint256 jobId => mapping(address pool => uint256 poolLockAmounts)) private poolLockAmounts; // lock amount for each pool + mapping(uint256 jobId => LockInfo lockInfo) private lockInfo; // total lock amount and unlock timestamp uint256 unlockEpoch; struct PoolConfig { uint256 weight; uint256 minStake; - bool isEnabled; + bool enabled; } // operator, lockToken, lockAmount should be stored in JobManager struct LockInfo { - uint256 lockAmount; // locked amount for the pool + uint256 totalLockAmount; uint256 unlockTimestamp; } @@ -74,47 +77,54 @@ contract StakingManager is // locked stake will be unlocked after an epoch if no slas result is submitted // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) - function onJobCreation(address _jobId, address _operator, address token, uint256 _lockAmount) external { + function onJobCreation(uint256 _jobId, address _operator, address token, uint256 _lockAmount) external { // TODO: only jobManager // TODO: lock operator selfstake (check how much) - address[] memory enabledPoolList = getEnabledPoolList(); - uint256 len = enabledPoolList.length; + uint256 len = stakingPoolSet.length(); for(uint256 i = 0; i < len; i++) { - pool = enabledPoolList[i]; + address pool = stakingPoolSet.at(i); + if(!isEnabledPool(pool)) continue; // skip if the token is not supported by the pool - if(!IKalypsoStakingPool(pool).isSupportedToken(token)) continue; + if(!IKalypsoStaking(pool).isSupportedToken(token)) continue; - uint256 poolStake = getPoolStake(pool, _operator, token); + uint256 poolStake = getPoolStake(pool, _operator, token); uint256 minStake = poolConfig[pool].minStake; // skip if the pool stake is less than the minStake + // TODO: let _lockStake calculate this check if(poolStake >= minStake) { // lock the stake uint256 lockAmount = _calcLockAmount(poolStake, poolConfig[pool].weight); // TODO: need to check formula for calculation // TODO: move fund from the pool (implement lockStake in each pool) // TODO: SymbioticStaking will just have empty code in it - _lockStake(pool, lockAmount); + _lockPoolStake(_jobId, pool, _lockAmount); } } } + + function getPoolStake(address _pool, address _operator, address _token) internal view returns (uint256) { + return IKalypsoStaking(_pool).getStakeAmount(_operator, _token); + } // TODO: make sure nothing happens when storing value only - function _lockStake(address _jobId, uint256 amount) internal { - lockInfo[_jobId].lockAmount += amount; + function _lockPoolStake(uint256 _jobId, address pool, uint256 amount) internal { + lockInfo[_jobId].totalLockAmount += amount; lockInfo[_jobId].unlockTimestamp = block.timestamp + unlockEpoch; } - function _unlockStake(address _jobId) internal { - lockInfo[_jobId].lockAmount = 0; + function _unlockStake(uint256 _jobId) internal { + lockInfo[_jobId].totalLockAmount = 0; lockInfo[_jobId].unlockTimestamp = 0; + + // TODO: send back fund } // called when job is completed to unlock the locked stakes - function onJobCompletion(address _jobId) external { + function onJobCompletion(uint256 _jobId) external { // TODO: only jobManager // TODO: unlock the locked stakes @@ -123,16 +133,15 @@ contract StakingManager is // when certain period has passed after the lock and no slash result is submitted, this can be unlocked // unlocking the locked stake does not check if token is enabled - function unlockStake(address _jobId) external { - // TODO: query if deadline has passed + no proof submission + not slashed - - uint256 len = stakingPoolSet.length; + function unlockStake(uint256 _jobId) external { + uint256 len = stakingPoolSet.length(); + address pool; for(uint256 i = 0; i < len; i++) { pool = stakingPoolSet.at(i); // unlock the stake - _unlockStake(pool, _jobId); + _unlockStake(_jobId); } } @@ -146,25 +155,16 @@ contract StakingManager is // TODO: check if the proof was submitted before the deadline, so need to query jobmanager } + function isEnabledPool(address _pool) public view returns (bool) { + return poolConfig[_pool].enabled; + } + /*======================================== Getter for Staking ========================================*/ function _calcLockAmount(uint256 amount, uint256 weight) internal pure returns (uint256) { return (amount * weight) / 10000; // TODO: need to check formula for calculation (probably be the share) } - - function getEnabledPoolList() public view returns (address[] memory enabledPoolList) { - uint256 len = stakingPoolSet.length; - for(uint256 i = 0; i < len; i++) { - pool = stakingPoolSet.at(i); - - if(poolConfig[pool].isEnabled) { - enabledPoolList.push(pool); - } - } - } - - /*======================================== Admin ========================================*/ // add new staking pool From 2964e1d48ac66b1103d99c677cc327aef6626630 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 3 Sep 2024 12:58:43 +0900 Subject: [PATCH 031/158] remove slashjob --- contracts/staking/l2_contracts/StakingManager.sol | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 9cbdd09..4958070 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -61,17 +61,6 @@ contract StakingManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); } - // TODO: Self stake is given to the slasher while the rest of the stakers is burnt when slashed - // TODO: check necessary params - function slashJob( - address _jobId, - address _vault, - uint256 _captureTimestamp, - uint256 _amount, - address _rewardAddress - ) external { - // TODO: only slashingManager - } // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) // locked stake will be unlocked after an epoch if no slas result is submitted @@ -82,7 +71,6 @@ contract StakingManager is // TODO: lock operator selfstake (check how much) - uint256 len = stakingPoolSet.length(); for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); From 40e4eee26cdc0c9dd985608df0ae2db88acddd81 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 3 Sep 2024 13:49:33 +0900 Subject: [PATCH 032/158] add lockPool to IKalypsoStaking --- .../interfaces/staking/IKalypsoStaking.sol | 3 ++- .../staking/l2_contracts/NativeStaking.sol | 8 +++++++ .../staking/l2_contracts/StakingManager.sol | 22 +++++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index f22d7fd..c123ce8 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.26; interface IKalypsoStaking { // function stakeOf(address _operator, address _token) external view returns (uint256); - function isSupportedToken(address _token) external view returns (bool); function getStakeAmount(address _operator, address _token) external view returns (uint256); function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts); + + function lockStake(address _operator, address _token, uint256 _amount) external; // Staking Manager only } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index ba85f2b..b6b11b5 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -228,4 +228,12 @@ contract NativeStaking is } // TODO: set staking manager + + /*======================================== StakingManager ========================================*/ + function lockStake(address _operator, address _token, uint256 _amount) external { + // TODO: StakingManager only + + // TODO: decide whether to move or to just store in the mapping + } + } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 4958070..22b5abb 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -61,7 +61,6 @@ contract StakingManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); } - // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) // locked stake will be unlocked after an epoch if no slas result is submitted @@ -74,22 +73,20 @@ contract StakingManager is uint256 len = stakingPoolSet.length(); for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - if(!isEnabledPool(pool)) continue; - - // skip if the token is not supported by the pool - if(!IKalypsoStaking(pool).isSupportedToken(token)) continue; + + if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled + if(!IKalypsoStaking(pool).isSupportedToken(token)) continue; // skip if the token is not supported by the pool uint256 poolStake = getPoolStake(pool, _operator, token); uint256 minStake = poolConfig[pool].minStake; // skip if the pool stake is less than the minStake - // TODO: let _lockStake calculate this check - if(poolStake >= minStake) { - // lock the stake + //? case when lockAmount > minStake? + if(poolStake >= minStake) { uint256 lockAmount = _calcLockAmount(poolStake, poolConfig[pool].weight); // TODO: need to check formula for calculation // TODO: move fund from the pool (implement lockStake in each pool) // TODO: SymbioticStaking will just have empty code in it - _lockPoolStake(_jobId, pool, _lockAmount); + _lockPoolStake(_jobId, pool, _operator, _lockAmount); } } } @@ -98,9 +95,10 @@ contract StakingManager is return IKalypsoStaking(_pool).getStakeAmount(_operator, _token); } - // TODO: make sure nothing happens when storing value only - function _lockPoolStake(uint256 _jobId, address pool, uint256 amount) internal { - lockInfo[_jobId].totalLockAmount += amount; + function _lockPoolStake(uint256 _jobId, address _pool, address _operator, uint256 _amount) internal { + // TODO: skip if the pool has less than minimum stake + + lockInfo[_jobId].totalLockAmount += _amount; lockInfo[_jobId].unlockTimestamp = block.timestamp + unlockEpoch; } From eb8a9af123a8e9cb41f2346c54fefc993068ea8a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 10 Sep 2024 23:36:20 +0900 Subject: [PATCH 033/158] add core feature in JobManager --- contracts/staking/l2_contracts/JobManager.sol | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 contracts/staking/l2_contracts/JobManager.sol diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol new file mode 100644 index 0000000..cf050e9 --- /dev/null +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; + +/* + JobManager contract is responsible for creating and managing jobs. + Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. + */ +contract JobManager { + + uint256 constant JOB_DURATION = 1 days; + + IStakingManager stakingManager; + + struct JobInfo { + address operator; + address token; + uint256 lockedAmount; + uint256 deadline; + // address transmitter; // this should be stored in StakingManager + } + + mapping(uint256 => JobInfo) public jobs; + + function createJob(uint256 jobId, address operator, address token, uint256 amountToLock) external { + // TODO: called only from Kalypso Protocol + + // TODO: create a job and record StakeData Transmitter who submitted capture timestamp + jobs[jobId] = JobInfo(operator, token, amountToLock, block.timestamp + JOB_DURATION); + + // TODO: call creation function in StakingManager + stakingManager.onJobCreation(jobId, operator, token, amountToLock); + } + + /** + * @notice Submit Multiple proofs in single transaction + */ + function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external { + require(jobIds.length == proofs.length, "Invalid Length"); + + for (uint256 index = 0; index < jobIds.length; index++) { + _verifyProof(jobIds[index], proofs[index]); + } + + // TODO: close job and distribute rewards + } + + /** + * @notice Submit Single Proof + */ + function submitProof(uint256 jobId, bytes calldata proof) public { + _verifyProof(jobId, proof); + } + + function _verifyProof(uint256 jobId, bytes calldata proof) internal { + // TODO + } + + function setStakingManager(address _stakingManager) external { + stakingManager = IStakingManager(_stakingManager); + } +} \ No newline at end of file From a65ea70edd6101dfef2cfe2f2355ee451fc433cf Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 12 Sep 2024 13:34:32 +0900 Subject: [PATCH 034/158] create SymbioticStakingReward contract --- .../l2_contracts/SymbioticStakingReward.sol | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 contracts/staking/l2_contracts/SymbioticStakingReward.sol diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol new file mode 100644 index 0000000..947b898 --- /dev/null +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +/* + Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, + which means the state of each vault address will be updated whenvever snapshot is submitted. +*/ + +contract SymbioticStakingReward { + //? what should be done when stake is locked? + // -> just update totalStakeAmount and rewardPerToken? + // -> does this even affect anything? + + // TODO: (mapping) captureTimestamp => token => vault => amount + + // TODO: (mapping) captureTimestamp => token => totalStakeAmount + + // TODO: (mapping) token => rewardPerToken + + // TODO: (array) confirmed timestamp + + // TODO: (function) function that updates confirmed timestamp + // this should be called by StakingManager contract when partial txs are completed + + // TODO: (function) updates reward per token for each submission + + // TODO: admin + + // TODO: claim +} \ No newline at end of file From 58908454c4ddf92851a69a6dbd2b57adce9f2783 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 12 Sep 2024 13:38:36 +0900 Subject: [PATCH 035/158] add upgradeable contracts inheritance --- .../l2_contracts/SymbioticStakingReward.sol | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 947b898..cee92e2 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -2,30 +2,72 @@ pragma solidity ^0.8.26; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + /* Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, which means the state of each vault address will be updated whenvever snapshot is submitted. */ -contract SymbioticStakingReward { +contract SymbioticStakingReward is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + UUPSUpgradeable +{ //? what should be done when stake is locked? // -> just update totalStakeAmount and rewardPerToken? // -> does this even affect anything? // TODO: (mapping) captureTimestamp => token => vault => amount - + // TODO: (mapping) captureTimestamp => token => totalStakeAmount - + // TODO: (mapping) token => rewardPerToken - + // TODO: (array) confirmed timestamp // TODO: (function) function that updates confirmed timestamp // this should be called by StakingManager contract when partial txs are completed - + // TODO: (function) updates reward per token for each submission - + // TODO: admin // TODO: claim -} \ No newline at end of file + + //-------------------------------- Init start --------------------------------// + + function initialize(address _admin) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + __ReentrancyGuard_init_unchained(); + __ReentrancyGuard_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + + //-------------------------------- Init end --------------------------------// + + + //-------------------------------- Overrides start --------------------------------// + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165Upgradeable, AccessControlUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + //-------------------------------- Overrides end --------------------------------// +} From 1738b0f54f761a4992b694074d5792634f7a1535 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 13 Sep 2024 12:32:59 +0900 Subject: [PATCH 036/158] add logic --- .../l2_contracts/SymbioticStakingReward.sol | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index cee92e2..734302a 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -9,6 +9,7 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; /* Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, which means the state of each vault address will be updated whenvever snapshot is submitted. @@ -22,30 +23,45 @@ contract SymbioticStakingReward is PausableUpgradeable, UUPSUpgradeable { + // TODO: staking token enability should be pulled from SymbioticStaking contract + + // TODO: fee token + // TODO: reward token + //? what should be done when stake is locked? // -> just update totalStakeAmount and rewardPerToken? // -> does this even affect anything? - // TODO: (mapping) captureTimestamp => token => vault => amount + mapping(address token => uint256 amount) public poolReward; + mapping(address token => uint256 rewardPerToken) public rewardPerTokens; + mapping(uint256 captureTimestamp => mapping(address token => uint256 totalStakeAmount)) public totalStakeAmount; - // TODO: (mapping) captureTimestamp => token => totalStakeAmount + // token supported by the vault can be queried in SymbioticStaking contract + mapping(uint256 captureTimestamp => mapping(address vault => uint256 amount)) public vaultStakeAmount; + mapping(uint256 captureTimestamp => mapping(address vault => uint256 rewardPerTokenPaid)) public vaultRewardPerTokenPaid; + mapping(address vault => uint256 reward) public vaultReward; - // TODO: (mapping) token => rewardPerToken + address public symbioticStaking; // TODO: (array) confirmed timestamp + // probably read from StakingManager // TODO: (function) function that updates confirmed timestamp // this should be called by StakingManager contract when partial txs are completed // TODO: (function) updates reward per token for each submission - // TODO: admin // TODO: claim + modifier onlySymbioticStaking() { + require(_msgSender() == symbioticStaking, "Caller is not the staking manager"); + _; + } + //-------------------------------- Init start --------------------------------// - function initialize(address _admin) public initializer { + function initialize(address _admin, address _stakingManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -54,11 +70,70 @@ contract SymbioticStakingReward is __ReentrancyGuard_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + symbioticStaking = _stakingManager; } //-------------------------------- Init end --------------------------------// + //-------------------------------- StakingManager start --------------------------------// + + /// @notice updates stake amount of a given vault + /// @notice valid only if captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking when submission is completed + /// @dev only can be called by SymbioticStaking contract + // TODO: check how to get _token address (probably gets pulled from SymbioticStkaing contract) + function updateVaultStakeAmount(uint256 _captureTimestamp, address _token, address _vault, uint256 _amount) external onlySymbioticStaking { + _update(_captureTimestamp, _vault, _token, _amount); + + vaultStakeAmount[_captureTimestamp][_vault] = _amount; + // TODO: emit events? + } + + function getLatestConfirmedTimestamp() external view returns (uint256) { + return _getLatestConfirmedTimestamp(); + } + + function _rewardPerToken(address _token) internal view returns (uint256) { + uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); + uint256 _totalStakeAmount = totalStakeAmount[_latestConfirmedTimestamp][_token]; + uint256 _rewardAmount = poolReward[_token]; + + // TODO: check + return _totalStakeAmount == 0 ? rewardPerTokens[_token] : rewardPerTokens[_token] + (_rewardAmount * 1e18) / _totalStakeAmount; + } + + function _getLatestConfirmedTimestamp() internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); + } + + //-------------------------------- StakingManager end --------------------------------// + + //-------------------------------- Update start --------------------------------// + + // called 1) when updated by StakingManager 2) + function _update(uint256 _captureTimestamp, address _vault, address _token, uint256 _totalStakeAmount) internal { + // TODO: update logic for add reward + + uint256 currentRewardPerToken = _rewardPerToken(_token); + rewardPerTokens[_token] = currentRewardPerToken; + + // update reward for each vault + vaultReward[_vault] += _pendingReward(_vault, _token); + vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; + } + + function _pendingReward(address _vault, address _token) internal view returns (uint256) { + uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); + uint256 _rewardPerTokenPaid = vaultRewardPerTokenPaid[_latestConfirmedTimestamp][_vault]; + uint256 _rewardPerToken = _rewardPerToken(_token); + + return (vaultStakeAmount[_latestConfirmedTimestamp][_vault] * (_rewardPerToken - _rewardPerTokenPaid)) / 1e18; // TODO muldiv + } + + //-------------------------------- Update end --------------------------------// + + //-------------------------------- Overrides start --------------------------------// function supportsInterface( @@ -70,4 +145,16 @@ contract SymbioticStakingReward is function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} //-------------------------------- Overrides end --------------------------------// + + //-------------------------------- Admin start --------------------------------// + function setStakingManager(address _stakingManager) external { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); + + symbioticStaking = _stakingManager; + // TODO: emit event + } + + //-------------------------------- Admin end --------------------------------// + + } From e239d012da11f32c7f1ddfe50a4f92d65f7cc636 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 13 Sep 2024 19:34:59 +0900 Subject: [PATCH 037/158] add _stakeToken and _rewardToken --- .../l2_contracts/SymbioticStakingReward.sol | 92 ++++++++++++++----- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 734302a..4a1eb5c 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -8,6 +8,8 @@ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/acce import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; /* @@ -23,6 +25,7 @@ contract SymbioticStakingReward is PausableUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; // TODO: staking token enability should be pulled from SymbioticStaking contract // TODO: fee token @@ -32,14 +35,25 @@ contract SymbioticStakingReward is // -> just update totalStakeAmount and rewardPerToken? // -> does this even affect anything? - mapping(address token => uint256 amount) public poolReward; - mapping(address token => uint256 rewardPerToken) public rewardPerTokens; - mapping(uint256 captureTimestamp => mapping(address token => uint256 totalStakeAmount)) public totalStakeAmount; - - // token supported by the vault can be queried in SymbioticStaking contract - mapping(uint256 captureTimestamp => mapping(address vault => uint256 amount)) public vaultStakeAmount; + /* + rewardToken: Fee Reward, Inflation Reward + stakeToken: staking token + */ + + // total amount staked for each stakeToken + // notice: the total amount can be reduced when a job is created and the stake is locked + mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public totalStakeAmounts; + // reward remaining for each stakeToken + mapping(address rewardToken => mapping(address stakeToken => uint256 amount)) public rewards; + // rewardTokens per stakeToken + mapping(address stakeToken => mapping(address rewardToken => uint256 amount)) public rewardPerTokens; + + // stakeToken supported by each vault should be queried in SymbioticStaking contract + mapping(uint256 captureTimestamp => mapping(address vault => uint256 amount)) public vaultStakeAmounts; + // rewardPerToken to store when update mapping(uint256 captureTimestamp => mapping(address vault => uint256 rewardPerTokenPaid)) public vaultRewardPerTokenPaid; - mapping(address vault => uint256 reward) public vaultReward; + // reward accrued that the vault can claim + mapping(address vault => uint256 rewardAmount) public claimableRewards; address public symbioticStaking; @@ -80,13 +94,15 @@ contract SymbioticStakingReward is //-------------------------------- StakingManager start --------------------------------// /// @notice updates stake amount of a given vault - /// @notice valid only if captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking when submission is completed + /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed /// @dev only can be called by SymbioticStaking contract // TODO: check how to get _token address (probably gets pulled from SymbioticStkaing contract) function updateVaultStakeAmount(uint256 _captureTimestamp, address _token, address _vault, uint256 _amount) external onlySymbioticStaking { - _update(_captureTimestamp, _vault, _token, _amount); + // TODO: update both of rewardTokens + // _update(_captureTimestamp, _vault, _token, _amount); - vaultStakeAmount[_captureTimestamp][_vault] = _amount; + vaultStakeAmounts[_captureTimestamp][_vault] = _amount; + // TODO: emit events? } @@ -94,15 +110,26 @@ contract SymbioticStakingReward is return _getLatestConfirmedTimestamp(); } - function _rewardPerToken(address _token) internal view returns (uint256) { + /// @notice returns stakeToken address of a given vault + function getVaultStakeToken(address _vault) external view returns (address) { + // TODO: pull from Symbioticstaking contract + } + + function lockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { + // TODO: set function for locking stake + } + + + function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); - uint256 _totalStakeAmount = totalStakeAmount[_latestConfirmedTimestamp][_token]; - uint256 _rewardAmount = poolReward[_token]; + uint256 _totalStakeAmount = totalStakeAmounts[_latestConfirmedTimestamp][_stakeToken]; + uint256 _rewardAmount = rewards[_rewardToken][_stakeToken]; - // TODO: check - return _totalStakeAmount == 0 ? rewardPerTokens[_token] : rewardPerTokens[_token] + (_rewardAmount * 1e18) / _totalStakeAmount; + // TODO: muldiv + return _totalStakeAmount == 0 ? rewardPerTokens[_stakeToken][_rewardToken] : rewardPerTokens[_stakeToken][_rewardToken] + (_rewardAmount * 1e18) / _totalStakeAmount; } + function _getLatestConfirmedTimestamp() internal view returns (uint256) { return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); } @@ -112,23 +139,25 @@ contract SymbioticStakingReward is //-------------------------------- Update start --------------------------------// // called 1) when updated by StakingManager 2) - function _update(uint256 _captureTimestamp, address _vault, address _token, uint256 _totalStakeAmount) internal { + function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken, uint256 _totalStakeAmount) internal { // TODO: update logic for add reward - uint256 currentRewardPerToken = _rewardPerToken(_token); - rewardPerTokens[_token] = currentRewardPerToken; + uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; - // update reward for each vault - vaultReward[_vault] += _pendingReward(_vault, _token); - vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; + if(_vault != address(0)) { + // update reward for each vault + claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); + vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; + } } - function _pendingReward(address _vault, address _token) internal view returns (uint256) { + function _pendingReward(address _vault, address _stakeToken, address _rewardToken) internal view returns (uint256) { uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); uint256 _rewardPerTokenPaid = vaultRewardPerTokenPaid[_latestConfirmedTimestamp][_vault]; - uint256 _rewardPerToken = _rewardPerToken(_token); + uint256 _rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - return (vaultStakeAmount[_latestConfirmedTimestamp][_vault] * (_rewardPerToken - _rewardPerTokenPaid)) / 1e18; // TODO muldiv + return (vaultStakeAmounts[_latestConfirmedTimestamp][_vault] * (_rewardPerToken - _rewardPerTokenPaid)) / 1e18; // TODO muldiv } //-------------------------------- Update end --------------------------------// @@ -157,4 +186,19 @@ contract SymbioticStakingReward is //-------------------------------- Admin end --------------------------------// + //-------------------------------- JobManager start --------------------------------// + + /// @notice JobManager add reward to the pool + function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external { + // TODO: Only JobManager + require(_stakeToken != address(0) || _rewardToken != address(0) , "zero address"); + require(_amount > 0, "zero amount"); + + IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); + + + } + + //-------------------------------- JobManager end --------------------------------// + } From a10c4c34b22a5c51ed234fdc90fdae0581ef8ff1 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 13 Sep 2024 20:52:22 +0900 Subject: [PATCH 038/158] import Math, EnumerableSet --- .../l2_contracts/SymbioticStakingReward.sol | 119 ++++++++++++------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 4a1eb5c..37fc6af 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -12,6 +12,10 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + /* Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, which means the state of each vault address will be updated whenvever snapshot is submitted. @@ -26,10 +30,12 @@ contract SymbioticStakingReward is UUPSUpgradeable { using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + using Math for uint256; + // TODO: staking token enability should be pulled from SymbioticStaking contract - - // TODO: fee token - // TODO: reward token + + EnumerableSet.AddressSet private _rewardTokenSet; //? what should be done when stake is locked? // -> just update totalStakeAmount and rewardPerToken? @@ -42,16 +48,18 @@ contract SymbioticStakingReward is // total amount staked for each stakeToken // notice: the total amount can be reduced when a job is created and the stake is locked - mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public totalStakeAmounts; + mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public + totalStakeAmounts; // reward remaining for each stakeToken mapping(address rewardToken => mapping(address stakeToken => uint256 amount)) public rewards; // rewardTokens per stakeToken - mapping(address stakeToken => mapping(address rewardToken => uint256 amount)) public rewardPerTokens; + mapping(address stakeToken => mapping(address rewardToken => uint256 amount)) public rewardPerTokens; // stakeToken supported by each vault should be queried in SymbioticStaking contract mapping(uint256 captureTimestamp => mapping(address vault => uint256 amount)) public vaultStakeAmounts; // rewardPerToken to store when update - mapping(uint256 captureTimestamp => mapping(address vault => uint256 rewardPerTokenPaid)) public vaultRewardPerTokenPaid; + mapping(uint256 captureTimestamp => mapping(address vault => uint256 rewardPerTokenPaid)) public + vaultRewardPerTokenPaid; // reward accrued that the vault can claim mapping(address vault => uint256 rewardAmount) public claimableRewards; @@ -65,7 +73,6 @@ contract SymbioticStakingReward is // TODO: (function) updates reward per token for each submission - // TODO: claim modifier onlySymbioticStaking() { @@ -90,19 +97,21 @@ contract SymbioticStakingReward is //-------------------------------- Init end --------------------------------// - //-------------------------------- StakingManager start --------------------------------// /// @notice updates stake amount of a given vault /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed /// @dev only can be called by SymbioticStaking contract // TODO: check how to get _token address (probably gets pulled from SymbioticStkaing contract) - function updateVaultStakeAmount(uint256 _captureTimestamp, address _token, address _vault, uint256 _amount) external onlySymbioticStaking { + function updateVaultStakeAmount(uint256 _captureTimestamp, address _token, address _vault, uint256 _amount) + external + onlySymbioticStaking + { // TODO: update both of rewardTokens // _update(_captureTimestamp, _vault, _token, _amount); vaultStakeAmounts[_captureTimestamp][_vault] = _amount; - + // TODO: emit events? } @@ -119,17 +128,18 @@ contract SymbioticStakingReward is // TODO: set function for locking stake } - + /// @notice rewardToken amount per stakeToken function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); uint256 _totalStakeAmount = totalStakeAmounts[_latestConfirmedTimestamp][_stakeToken]; uint256 _rewardAmount = rewards[_rewardToken][_stakeToken]; // TODO: muldiv - return _totalStakeAmount == 0 ? rewardPerTokens[_stakeToken][_rewardToken] : rewardPerTokens[_stakeToken][_rewardToken] + (_rewardAmount * 1e18) / _totalStakeAmount; + return _totalStakeAmount == 0 + ? rewardPerTokens[_stakeToken][_rewardToken] + : rewardPerTokens[_stakeToken][_rewardToken] + _rewardAmount.mulDiv(1e18, _totalStakeAmount); } - function _getLatestConfirmedTimestamp() internal view returns (uint256) { return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); } @@ -138,40 +148,47 @@ contract SymbioticStakingReward is //-------------------------------- Update start --------------------------------// - // called 1) when updated by StakingManager 2) - function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken, uint256 _totalStakeAmount) internal { - // TODO: update logic for add reward - + function _update( + uint256 _captureTimestamp, + address _vault, + address _stakeToken, + address _rewardToken + ) internal { uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; - if(_vault != address(0)) { - // update reward for each vault - claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); - vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; - } + // update reward for each vault + claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); + vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; } - function _pendingReward(address _vault, address _stakeToken, address _rewardToken) internal view returns (uint256) { - uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); - uint256 _rewardPerTokenPaid = vaultRewardPerTokenPaid[_latestConfirmedTimestamp][_vault]; - uint256 _rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + function _pendingReward(address _vault, address _stakeToken, address _rewardToken) + internal + view + returns (uint256) + { + uint256 latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); + uint256 rewardPerTokenPaid = vaultRewardPerTokenPaid[latestConfirmedTimestamp][_vault]; + uint256 rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - return (vaultStakeAmounts[_latestConfirmedTimestamp][_vault] * (_rewardPerToken - _rewardPerTokenPaid)) / 1e18; // TODO muldiv + return (vaultStakeAmounts[latestConfirmedTimestamp][_vault].mulDiv((rewardPerToken - rewardPerTokenPaid), 1e18)); } //-------------------------------- Update end --------------------------------// - //-------------------------------- Overrides start --------------------------------// - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC165Upgradeable, AccessControlUpgradeable) returns (bool) { + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { return super.supportsInterface(interfaceId); } - function _authorizeUpgrade(address /*account*/) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} //-------------------------------- Overrides end --------------------------------// @@ -179,26 +196,54 @@ contract SymbioticStakingReward is function setStakingManager(address _stakingManager) external { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); - symbioticStaking = _stakingManager; + _setStakingManager(_stakingManager); // TODO: emit event } + function _setStakingManager(address _stakingManager) internal { + symbioticStaking = _stakingManager; + } + + function _addRewardToken(address _rewardToken) internal { + _rewardTokenSet.add(_rewardToken); + } + + function _removeRewardToken(address _rewardToken) internal { + _rewardTokenSet.remove(_rewardToken); + } + //-------------------------------- Admin end --------------------------------// + //-------------------------------- Getter start --------------------------------// + + function getRewardTokens() external view returns (address[] memory) { + address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); + for (uint256 i = 0; i < _rewardTokenSet.length(); i++) { + _rewardTokens[i] = _rewardTokenSet.at(i); + } + return _rewardTokens; + } + + function isSupportedRewardToken(address _rewardToken) public view returns (bool) { + return _rewardTokenSet.contains(_rewardToken); + } + + //-------------------------------- Getter end --------------------------------// //-------------------------------- JobManager start --------------------------------// - /// @notice JobManager add reward to the pool + /// @notice JobManager adds reward to the pool function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external { // TODO: Only JobManager - require(_stakeToken != address(0) || _rewardToken != address(0) , "zero address"); + + require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); require(_amount > 0, "zero amount"); IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); - + uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; } //-------------------------------- JobManager end --------------------------------// - } From d6ebec65f056ec37201e47abe1d11c8d8a445eee Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 00:01:04 +0900 Subject: [PATCH 039/158] add active stake and more update logic --- .../l2_contracts/SymbioticStakingReward.sol | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 37fc6af..58198ce 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -50,6 +50,9 @@ contract SymbioticStakingReward is // notice: the total amount can be reduced when a job is created and the stake is locked mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public totalStakeAmounts; + // locked amount for each stakeToken upon job creation + mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public + lockedStakeAmounts; // reward remaining for each stakeToken mapping(address rewardToken => mapping(address stakeToken => uint256 amount)) public rewards; // rewardTokens per stakeToken @@ -107,16 +110,21 @@ contract SymbioticStakingReward is external onlySymbioticStaking { - // TODO: update both of rewardTokens - // _update(_captureTimestamp, _vault, _token, _amount); + // update reward for each rewardToken + uint256 len = _rewardTokenSet.length(); + for (uint256 i = 0; i < len; i++) { + address rewardToken = _rewardTokenSet.at(i); + _update(_captureTimestamp, _vault, _token, rewardToken); + } vaultStakeAmounts[_captureTimestamp][_vault] = _amount; + totalStakeAmounts[_captureTimestamp][_token] += _amount; - // TODO: emit events? + // TODO: emit event } function getLatestConfirmedTimestamp() external view returns (uint256) { - return _getLatestConfirmedTimestamp(); + return _latestConfirmedTimestamp(); } /// @notice returns stakeToken address of a given vault @@ -125,22 +133,38 @@ contract SymbioticStakingReward is } function lockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { - // TODO: set function for locking stake + lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] += amount; + + // TODO: update rewardPerToken for each rewardToken + _updateRewardPerTokens(_stakeToken); + } + + function _updateRewardPerTokens(address _stakeToken) internal { + uint256 len = _rewardTokenSet.length(); + for(uint256 i = 0; i < len; i++) { + address rewardToken = _rewardTokenSet.at(i); + rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); + } + } + + function unlockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { + lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] -= amount; + + _updateRewardPerTokens(_stakeToken); } /// @notice rewardToken amount per stakeToken function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { - uint256 _latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); - uint256 _totalStakeAmount = totalStakeAmounts[_latestConfirmedTimestamp][_stakeToken]; - uint256 _rewardAmount = rewards[_rewardToken][_stakeToken]; + uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); + uint256 totalStakeAmount = totalStakeAmountsActive(_stakeToken); + uint256 rewardAmount = rewards[_rewardToken][_stakeToken]; - // TODO: muldiv - return _totalStakeAmount == 0 + return totalStakeAmount == 0 ? rewardPerTokens[_stakeToken][_rewardToken] - : rewardPerTokens[_stakeToken][_rewardToken] + _rewardAmount.mulDiv(1e18, _totalStakeAmount); + : rewardPerTokens[_stakeToken][_rewardToken] + rewardAmount.mulDiv(1e18, totalStakeAmount); } - function _getLatestConfirmedTimestamp() internal view returns (uint256) { + function _latestConfirmedTimestamp() internal view returns (uint256) { return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); } @@ -148,12 +172,8 @@ contract SymbioticStakingReward is //-------------------------------- Update start --------------------------------// - function _update( - uint256 _captureTimestamp, - address _vault, - address _stakeToken, - address _rewardToken - ) internal { + function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { + // update rewardPerToken uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; @@ -167,7 +187,7 @@ contract SymbioticStakingReward is view returns (uint256) { - uint256 latestConfirmedTimestamp = _getLatestConfirmedTimestamp(); + uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); uint256 rewardPerTokenPaid = vaultRewardPerTokenPaid[latestConfirmedTimestamp][_vault]; uint256 rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); @@ -218,7 +238,8 @@ contract SymbioticStakingReward is function getRewardTokens() external view returns (address[] memory) { address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); - for (uint256 i = 0; i < _rewardTokenSet.length(); i++) { + uint256 len = _rewardTokenSet.length(); + for (uint256 i = 0; i < len; i++) { _rewardTokens[i] = _rewardTokenSet.at(i); } return _rewardTokens; @@ -228,6 +249,12 @@ contract SymbioticStakingReward is return _rewardTokenSet.contains(_rewardToken); } + function totalStakeAmountsActive(address _stakeToken) public view returns (uint256) { + uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); + return totalStakeAmounts[latestConfirmedTimestamp][_stakeToken] + - lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken]; + } + //-------------------------------- Getter end --------------------------------// //-------------------------------- JobManager start --------------------------------// @@ -241,6 +268,7 @@ contract SymbioticStakingReward is IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); + // update rewardPerToken uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; } From c788c5183c7ec72770d8ec3a22170132591ba0d7 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 00:06:10 +0900 Subject: [PATCH 040/158] refactor reward logic --- .../l2_contracts/SymbioticStakingReward.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 58198ce..9af2244 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -85,6 +85,7 @@ contract SymbioticStakingReward is //-------------------------------- Init start --------------------------------// + // TODO: initialize contract addresses function initialize(address _admin, address _stakingManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); @@ -105,8 +106,7 @@ contract SymbioticStakingReward is /// @notice updates stake amount of a given vault /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed /// @dev only can be called by SymbioticStaking contract - // TODO: check how to get _token address (probably gets pulled from SymbioticStkaing contract) - function updateVaultStakeAmount(uint256 _captureTimestamp, address _token, address _vault, uint256 _amount) + function updateVaultStakeAmount(uint256 _captureTimestamp, address _stakeToken, address _vault, uint256 _amount) external onlySymbioticStaking { @@ -114,11 +114,11 @@ contract SymbioticStakingReward is uint256 len = _rewardTokenSet.length(); for (uint256 i = 0; i < len; i++) { address rewardToken = _rewardTokenSet.at(i); - _update(_captureTimestamp, _vault, _token, rewardToken); + _update(_captureTimestamp, _vault, _stakeToken, rewardToken); } vaultStakeAmounts[_captureTimestamp][_vault] = _amount; - totalStakeAmounts[_captureTimestamp][_token] += _amount; + totalStakeAmounts[_captureTimestamp][_stakeToken] += _amount; // TODO: emit event } @@ -127,15 +127,10 @@ contract SymbioticStakingReward is return _latestConfirmedTimestamp(); } - /// @notice returns stakeToken address of a given vault - function getVaultStakeToken(address _vault) external view returns (address) { - // TODO: pull from Symbioticstaking contract - } function lockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] += amount; - // TODO: update rewardPerToken for each rewardToken _updateRewardPerTokens(_stakeToken); } @@ -155,7 +150,6 @@ contract SymbioticStakingReward is /// @notice rewardToken amount per stakeToken function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { - uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); uint256 totalStakeAmount = totalStakeAmountsActive(_stakeToken); uint256 rewardAmount = rewards[_rewardToken][_stakeToken]; From 467efcd8702d8ba7c5972297a02fec02125b2e6f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 00:32:26 +0900 Subject: [PATCH 041/158] refactor admin functions --- .../l2_contracts/SymbioticStakingReward.sol | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 9af2244..1c64ab1 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -67,22 +67,29 @@ contract SymbioticStakingReward is mapping(address vault => uint256 rewardAmount) public claimableRewards; address public symbioticStaking; + address public jobManager; - // TODO: (array) confirmed timestamp - // probably read from StakingManager - - // TODO: (function) function that updates confirmed timestamp - // this should be called by StakingManager contract when partial txs are completed - - // TODO: (function) updates reward per token for each submission // TODO: claim + // TODO: vault => claimAddress + modifier onlySymbioticStaking() { require(_msgSender() == symbioticStaking, "Caller is not the staking manager"); _; } + modifier onlyJobManager() { + require(_msgSender() == jobManager, "Caller is not the job manager"); + _; + } + + modifier onlyAdmin() { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); + _; + } + + //-------------------------------- Init start --------------------------------// // TODO: initialize contract addresses @@ -134,13 +141,7 @@ contract SymbioticStakingReward is _updateRewardPerTokens(_stakeToken); } - function _updateRewardPerTokens(address _stakeToken) internal { - uint256 len = _rewardTokenSet.length(); - for(uint256 i = 0; i < len; i++) { - address rewardToken = _rewardTokenSet.at(i); - rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); - } - } + function unlockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] -= amount; @@ -164,6 +165,24 @@ contract SymbioticStakingReward is //-------------------------------- StakingManager end --------------------------------// + //-------------------------------- JobManager start --------------------------------// + + /// @notice JobManager adds reward to the pool + function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external { + // TODO: Only JobManager + + require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); + require(_amount > 0, "zero amount"); + + IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); + + // update rewardPerToken + uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; + } + + //-------------------------------- JobManager end --------------------------------// + //-------------------------------- Update start --------------------------------// function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { @@ -176,6 +195,14 @@ contract SymbioticStakingReward is vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; } + function _updateRewardPerTokens(address _stakeToken) internal { + uint256 len = _rewardTokenSet.length(); + for(uint256 i = 0; i < len; i++) { + address rewardToken = _rewardTokenSet.at(i); + rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); + } + } + function _pendingReward(address _vault, address _stakeToken, address _rewardToken) internal view @@ -207,22 +234,21 @@ contract SymbioticStakingReward is //-------------------------------- Overrides end --------------------------------// //-------------------------------- Admin start --------------------------------// - function setStakingManager(address _stakingManager) external { - require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); - - _setStakingManager(_stakingManager); + function setStakingManager(address _stakingManager) public onlyAdmin { + symbioticStaking = _stakingManager; // TODO: emit event } - function _setStakingManager(address _stakingManager) internal { - symbioticStaking = _stakingManager; + function setJobManager(address _jobManager) public onlyAdmin { + jobManager = _jobManager; + // TODO: emit event } - function _addRewardToken(address _rewardToken) internal { + function addRewardToken(address _rewardToken) external onlyAdmin { _rewardTokenSet.add(_rewardToken); } - function _removeRewardToken(address _rewardToken) internal { + function removeRewardToken(address _rewardToken) external onlyAdmin { _rewardTokenSet.remove(_rewardToken); } @@ -251,21 +277,5 @@ contract SymbioticStakingReward is //-------------------------------- Getter end --------------------------------// - //-------------------------------- JobManager start --------------------------------// - - /// @notice JobManager adds reward to the pool - function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external { - // TODO: Only JobManager - - require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); - require(_amount > 0, "zero amount"); - IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); - - // update rewardPerToken - uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; - } - - //-------------------------------- JobManager end --------------------------------// } From 6525aa5db795e78746308edd191833d72e2cd14b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 01:09:42 +0900 Subject: [PATCH 042/158] refactor code --- .../l2_contracts/SymbioticStakingReward.sol | 120 +++++++++++------- 1 file changed, 74 insertions(+), 46 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 1c64ab1..9f36725 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -70,10 +70,7 @@ contract SymbioticStakingReward is address public jobManager; - // TODO: claim - // TODO: vault => claimAddress - modifier onlySymbioticStaking() { require(_msgSender() == symbioticStaking, "Caller is not the staking manager"); _; @@ -93,7 +90,7 @@ contract SymbioticStakingReward is //-------------------------------- Init start --------------------------------// // TODO: initialize contract addresses - function initialize(address _admin, address _stakingManager) public initializer { + function initialize(address _admin, address _stakingManager, address _jobManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -103,7 +100,8 @@ contract SymbioticStakingReward is _grantRole(DEFAULT_ADMIN_ROLE, _admin); - symbioticStaking = _stakingManager; + _setStakingManager(_stakingManager); + _setJobManager(_jobManager); } //-------------------------------- Init end --------------------------------// @@ -116,7 +114,10 @@ contract SymbioticStakingReward is function updateVaultStakeAmount(uint256 _captureTimestamp, address _stakeToken, address _vault, uint256 _amount) external onlySymbioticStaking - { + { + require(_captureTimestamp > 0, "zero timestamp"); + require(_stakeToken != address(0) || _vault != address(0), "zero address"); + // update reward for each rewardToken uint256 len = _rewardTokenSet.length(); for (uint256 i = 0; i < len; i++) { @@ -130,23 +131,55 @@ contract SymbioticStakingReward is // TODO: emit event } - function getLatestConfirmedTimestamp() external view returns (uint256) { - return _latestConfirmedTimestamp(); + function claimReward(address _vault) external { + uint256 rewardAmount = claimableRewards[_vault]; + require(rewardAmount > 0, "no reward to claim"); + + claimableRewards[_vault] = 0; + + // TODO + IERC20(_getRewardToken(_vault)).safeTransfer(_vault, rewardAmount); + } + + // TODO: decide where to store vault => rewardToken + function _getRewardToken(address vault) internal view returns (address) { + // ISymbioticStaking(symbioticStaking).rewardToken(vault); + } + + + function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external onlySymbioticStaking { + require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); + require(_amount > 0, "zero amount"); + + IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); + + // update rewardPerToken + uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; + + // TODO: emit event } function lockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { + require(amount <= totalStakeAmountsActive(_stakeToken), "insufficient stake amount"); + lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] += amount; _updateRewardPerTokens(_stakeToken); - } - + // TODO: emit event + } function unlockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { - lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] -= amount; + uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); + require(amount <= lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken], "insufficient locked stake amount"); + + lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken] -= amount; _updateRewardPerTokens(_stakeToken); + + // TODO: emit event } /// @notice rewardToken amount per stakeToken @@ -165,27 +198,11 @@ contract SymbioticStakingReward is //-------------------------------- StakingManager end --------------------------------// - //-------------------------------- JobManager start --------------------------------// - - /// @notice JobManager adds reward to the pool - function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external { - // TODO: Only JobManager - - require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); - require(_amount > 0, "zero amount"); - - IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); - - // update rewardPerToken - uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; - } - - //-------------------------------- JobManager end --------------------------------// - //-------------------------------- Update start --------------------------------// function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { + require(isSupportedRewardToken(_rewardToken), "unsupported reward token"); + // update rewardPerToken uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; @@ -217,29 +234,21 @@ contract SymbioticStakingReward is //-------------------------------- Update end --------------------------------// - //-------------------------------- Overrides start --------------------------------// - - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC165Upgradeable, AccessControlUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId); - } - - function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} - - //-------------------------------- Overrides end --------------------------------// - //-------------------------------- Admin start --------------------------------// function setStakingManager(address _stakingManager) public onlyAdmin { + _setStakingManager(_stakingManager); + } + + function _setStakingManager(address _stakingManager) internal { symbioticStaking = _stakingManager; // TODO: emit event } function setJobManager(address _jobManager) public onlyAdmin { + _setJobManager(_jobManager); + } + + function _setJobManager(address _jobManager) public onlyAdmin { jobManager = _jobManager; // TODO: emit event } @@ -256,6 +265,11 @@ contract SymbioticStakingReward is //-------------------------------- Getter start --------------------------------// + // TODO: needed? + function getLatestConfirmedTimestamp() external view returns (uint256) { + return _latestConfirmedTimestamp(); + } + function getRewardTokens() external view returns (address[] memory) { address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); uint256 len = _rewardTokenSet.length(); @@ -277,5 +291,19 @@ contract SymbioticStakingReward is //-------------------------------- Getter end --------------------------------// + //-------------------------------- Overrides start --------------------------------// -} + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + //-------------------------------- Overrides end --------------------------------// +} \ No newline at end of file From c2060cb788b62785f4300eeefd88ba1fd0d1d8fb Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 17:06:38 +0900 Subject: [PATCH 043/158] create NativeStakingReward contract --- .../l2_contracts/NativeStakingReward.sol | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 contracts/staking/l2_contracts/NativeStakingReward.sol diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol new file mode 100644 index 0000000..26bc53a --- /dev/null +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; + +contract NativeStakingReward is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + UUPSUpgradeable +{ + //-------------------------------- Overrides start --------------------------------// + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + //-------------------------------- Overrides end --------------------------------// +} \ No newline at end of file From 5c003148160d5a2f0f8f774b1605e0c1018dab44 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 17:10:14 +0900 Subject: [PATCH 044/158] remove everything related to jobManager --- .../l2_contracts/SymbioticStakingReward.sol | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 9f36725..f7fc2f7 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -67,7 +67,6 @@ contract SymbioticStakingReward is mapping(address vault => uint256 rewardAmount) public claimableRewards; address public symbioticStaking; - address public jobManager; // TODO: vault => claimAddress @@ -76,10 +75,6 @@ contract SymbioticStakingReward is _; } - modifier onlyJobManager() { - require(_msgSender() == jobManager, "Caller is not the job manager"); - _; - } modifier onlyAdmin() { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); @@ -88,9 +83,8 @@ contract SymbioticStakingReward is //-------------------------------- Init start --------------------------------// - // TODO: initialize contract addresses - function initialize(address _admin, address _stakingManager, address _jobManager) public initializer { + function initialize(address _admin, address _stakingManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -101,13 +95,10 @@ contract SymbioticStakingReward is _grantRole(DEFAULT_ADMIN_ROLE, _admin); _setStakingManager(_stakingManager); - _setJobManager(_jobManager); } - //-------------------------------- Init end --------------------------------// //-------------------------------- StakingManager start --------------------------------// - /// @notice updates stake amount of a given vault /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed /// @dev only can be called by SymbioticStaking contract @@ -139,6 +130,8 @@ contract SymbioticStakingReward is // TODO IERC20(_getRewardToken(_vault)).safeTransfer(_vault, rewardAmount); + + // TODO: emit event } // TODO: decide where to store vault => rewardToken @@ -146,7 +139,6 @@ contract SymbioticStakingReward is // ISymbioticStaking(symbioticStaking).rewardToken(vault); } - function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external onlySymbioticStaking { require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); require(_amount > 0, "zero amount"); @@ -244,15 +236,6 @@ contract SymbioticStakingReward is // TODO: emit event } - function setJobManager(address _jobManager) public onlyAdmin { - _setJobManager(_jobManager); - } - - function _setJobManager(address _jobManager) public onlyAdmin { - jobManager = _jobManager; - // TODO: emit event - } - function addRewardToken(address _rewardToken) external onlyAdmin { _rewardTokenSet.add(_rewardToken); } From 59e620924596bad95a27bbda88ed6e8c573d55c7 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 14 Sep 2024 17:10:55 +0900 Subject: [PATCH 045/158] add initialize --- .../staking/l2_contracts/NativeStakingReward.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 26bc53a..2933fe1 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -24,6 +24,22 @@ contract NativeStakingReward is PausableUpgradeable, UUPSUpgradeable { + + //-------------------------------- Init start --------------------------------// + + function initialize(address _admin, address _stakingManager) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + __ReentrancyGuard_init_unchained(); + __ReentrancyGuard_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + //-------------------------------- Init end --------------------------------// + + //-------------------------------- Overrides start --------------------------------// function supportsInterface(bytes4 interfaceId) From 3fe574e929741b2112d7d5e4120a0144c16b7dc0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sun, 15 Sep 2024 01:31:03 +0900 Subject: [PATCH 046/158] add rewardPerToken logic --- .../l2_contracts/NativeStakingReward.sol | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 2933fe1..36490b7 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -24,10 +24,28 @@ contract NativeStakingReward is PausableUpgradeable, UUPSUpgradeable { + address public nativeStaking; + + // TODO: (state) rewardPerToken + mapping(address token => mapping(address operator => uint256 amount)) operatorRewardAmounts; + mapping(address token => mapping(address operator => uint256 amount)) rewardPerTokens; + mapping(address account => mapping(address token => mapping(address operator => uint256 amount))) rewardPerTokenStored; + + // TODO: (state) rewardPerTokenStored + + // TODO: (function) stake + + // TODO: (function) unstake + + // TODO: (function) claim reward + + // TODO: (function) update + + // TODO: (function) addReward //-------------------------------- Init start --------------------------------// - function initialize(address _admin, address _stakingManager) public initializer { + function initialize(address _admin, address _nativeStaking) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -36,9 +54,67 @@ contract NativeStakingReward is __ReentrancyGuard_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _setNativeStaking(_nativeStaking); } //-------------------------------- Init end --------------------------------// + //-------------------------------- NativeStaking start --------------------------------// + + function stake(address token, uint256 amount) public { + // INativeStaking(nativeStaking).stake(token, amount); + } + + function lockStake(address token, uint256 amount, uint256 lockDuration) public { + // INativeStaking(nativeStaking).lockStake(token, amount, lockDuration); + } + + function unlockStake(address token, uint256 amount) public { + // INativeStaking(nativeStaking).unlockStake(token, amount); + } + + function unstake(address token, uint256 amount) public { + // INativeStaking(nativeStaking).unstake(token, amount); + } + + function claimStake(address token) public { + // INativeStaking(nativeStaking).claimStake(token); + } + + function _update(address account, address token, address operator) internal { + uint256 currentRewardPerToken = _rewardPerToken(token, operator); + } + + function _rewardPerToken(address _token, address _operator) internal view returns (uint256) { + uint256 totalStakeAmount = _getTotalStakeAmountActive(_token, _operator); + uint256 totalRewardAmount = operatorRewardAmounts[_token][_operator]; + + // TODO: muldiv + return totalStakeAmount == 0 ? rewardPerTokens[_token][_operator] : rewardPerTokens[_token][_operator] + totalRewardAmount / totalStakeAmount; + } + + function _getTotalStakeAmountActive(address token, address operator) internal view returns (uint256) { + // return INativeStaking(nativeStaking).getTotalStakeAmountActive(token, operator); + } + + function _getDelegatedStakeActive(address account, address token, address operator) internal view returns (uint256) { + // return INativeStaking(nativeStaking).getDelegatedStakeActive(account, token, operator); + } + + //-------------------------------- NativeStaking end --------------------------------// + + + //-------------------------------- Overrides start --------------------------------// + function setNativeStaking(address _nativeStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { + _setNativeStaking(_nativeStaking); + } + + function _setNativeStaking(address _nativeStaking) internal { + nativeStaking = _nativeStaking; + // TODO: emit event + } + + //-------------------------------- Overrides end --------------------------------// + //-------------------------------- Overrides start --------------------------------// From 46369f62d13c074253f4da33be21a606d61c2423 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 16 Sep 2024 06:58:53 +0800 Subject: [PATCH 047/158] fix mappings structure --- .../l2_contracts/NativeStakingReward.sol | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 36490b7..86fd28a 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -24,14 +24,17 @@ contract NativeStakingReward is PausableUpgradeable, UUPSUpgradeable { + using Math for uint256; + using SafeERC20 for IERC20; + address public nativeStaking; - // TODO: (state) rewardPerToken - mapping(address token => mapping(address operator => uint256 amount)) operatorRewardAmounts; - mapping(address token => mapping(address operator => uint256 amount)) rewardPerTokens; - mapping(address account => mapping(address token => mapping(address operator => uint256 amount))) rewardPerTokenStored; + // reward is accrued per operator + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) rewards; + // rewardTokens amount per stakeToken + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) rewardPerTokens; - // TODO: (state) rewardPerTokenStored + mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) rewardPerTokenPaid; // TODO: (function) stake @@ -82,27 +85,34 @@ contract NativeStakingReward is function _update(address account, address token, address operator) internal { uint256 currentRewardPerToken = _rewardPerToken(token, operator); + + } function _rewardPerToken(address _token, address _operator) internal view returns (uint256) { uint256 totalStakeAmount = _getTotalStakeAmountActive(_token, _operator); uint256 totalRewardAmount = operatorRewardAmounts[_token][_operator]; - // TODO: muldiv - return totalStakeAmount == 0 ? rewardPerTokens[_token][_operator] : rewardPerTokens[_token][_operator] + totalRewardAmount / totalStakeAmount; + // TODO: make sure decimal is 18 + return totalStakeAmount == 0 + ? rewardPerTokens[_token][_operator] + : rewardPerTokens[_token][_operator] + totalRewardAmount.mulDiv(1e18, totalStakeAmount); } function _getTotalStakeAmountActive(address token, address operator) internal view returns (uint256) { // return INativeStaking(nativeStaking).getTotalStakeAmountActive(token, operator); } - function _getDelegatedStakeActive(address account, address token, address operator) internal view returns (uint256) { + function _getDelegatedStakeActive(address account, address token, address operator) + internal + view + returns (uint256) + { // return INativeStaking(nativeStaking).getDelegatedStakeActive(account, token, operator); } //-------------------------------- NativeStaking end --------------------------------// - //-------------------------------- Overrides start --------------------------------// function setNativeStaking(address _nativeStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { _setNativeStaking(_nativeStaking); @@ -115,7 +125,6 @@ contract NativeStakingReward is //-------------------------------- Overrides end --------------------------------// - //-------------------------------- Overrides start --------------------------------// function supportsInterface(bytes4 interfaceId) @@ -131,4 +140,4 @@ contract NativeStakingReward is function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} //-------------------------------- Overrides end --------------------------------// -} \ No newline at end of file +} From 66a646c076afb0efec058257123a79d465e41af7 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 16 Sep 2024 08:03:04 +0800 Subject: [PATCH 048/158] add update logic --- .../l2_contracts/NativeStakingReward.sol | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 86fd28a..b962051 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -34,7 +34,8 @@ contract NativeStakingReward is // rewardTokens amount per stakeToken mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) rewardPerTokens; - mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) rewardPerTokenPaid; + mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) userRewardPerTokenPaid; + mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount)))) claimableRewards; // TODO: (function) stake @@ -83,26 +84,41 @@ contract NativeStakingReward is // INativeStaking(nativeStaking).claimStake(token); } - function _update(address account, address token, address operator) internal { - uint256 currentRewardPerToken = _rewardPerToken(token, operator); - + function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { + uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _operator, _rewardToken); + rewardPerTokens[_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + claimableRewards[account][_stakeToken][_operator][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); + userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + + } + + function _pendingReward(address account, address _stakeToken, address operator, address _rewardToken) internal view returns (uint256) { + uint256 rewardPerTokenPaid = userRewardPerTokenPaid[account][_stakeToken][operator][_rewardToken]; + uint256 rewardPerToken = _rewardPerToken(_stakeToken, operator, _rewardToken); + uint256 userStakeAmount = _getUserStakeAmount(account, _stakeToken, operator); + + return userStakeAmount.mulDiv(rewardPerToken - rewardPerTokenPaid, 1e18); } - function _rewardPerToken(address _token, address _operator) internal view returns (uint256) { - uint256 totalStakeAmount = _getTotalStakeAmountActive(_token, _operator); - uint256 totalRewardAmount = operatorRewardAmounts[_token][_operator]; + function _rewardPerToken(address _stakeToken, address _operator, address _rewardToken) internal view returns (uint256) { + uint256 totalStakeAmount = _getTotalStakeAmountActive(_stakeToken, _operator); + uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; // TODO: make sure decimal is 18 return totalStakeAmount == 0 - ? rewardPerTokens[_token][_operator] - : rewardPerTokens[_token][_operator] + totalRewardAmount.mulDiv(1e18, totalStakeAmount); + ? rewardPerTokens[_stakeToken][_operator][_rewardToken] + : rewardPerTokens[_stakeToken][_operator][_rewardToken] + totalRewardAmount.mulDiv(1e18, totalStakeAmount); } function _getTotalStakeAmountActive(address token, address operator) internal view returns (uint256) { // return INativeStaking(nativeStaking).getTotalStakeAmountActive(token, operator); } + function _getUserStakeAmount(address account, address token, address operator) internal view returns (uint256) { + // return INativeStaking(nativeStaking).getUserStakeAmount(account, token, operator); + } + function _getDelegatedStakeActive(address account, address token, address operator) internal view From dac1008a40bac43a010b2cb967f80f90e6b405ae Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 17 Sep 2024 12:34:17 +0800 Subject: [PATCH 049/158] remove lock getting deducted in RewardDistributors --- .../interfaces/staking/IKalypsoStaking.sol | 16 +++- .../interfaces/staking/INativeStaking.sol | 2 +- .../staking/INativeStakingReward.sol | 7 ++ .../interfaces/staking/IStakingManager.sol | 13 +++ .../interfaces/staking/ISymbioticStaking.sol | 13 ++- contracts/staking/l2_contracts/JobManager.sol | 8 +- .../staking/l2_contracts/NativeStaking.sol | 52 ++++++++--- .../l2_contracts/NativeStakingReward.sol | 20 ++++- .../staking/l2_contracts/StakingManager.sol | 88 +++++++++---------- .../staking/l2_contracts/SymbioticStaking.sol | 84 +++++++++++++++--- .../l2_contracts/SymbioticStakingReward.sol | 6 +- 11 files changed, 224 insertions(+), 85 deletions(-) create mode 100644 contracts/interfaces/staking/INativeStakingReward.sol diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index c123ce8..d30c78f 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -2,12 +2,20 @@ pragma solidity ^0.8.26; interface IKalypsoStaking { - // function stakeOf(address _operator, address _token) external view returns (uint256); function isSupportedToken(address _token) external view returns (bool); - function getStakeAmount(address _operator, address _token) external view returns (uint256); + function getPoolStake(address _operator, address _token) external view returns (uint256); - function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts); + function lockStake(uint256 jobId, address operator, address _token, uint256 _amount) external; // Staking Manager only - function lockStake(address _operator, address _token, uint256 _amount) external; // Staking Manager only + struct PoolLockInfo { + address token; + uint256 amount; + address transmitter; + } + + // struct NativeStakingLock { + // address token; + // uint256 amount; + // } } \ No newline at end of file diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index bc25f06..071f81b 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; interface INativeStaking is IKalypsoStaking { - struct OperatorStakeInfo { + struct OperatorStake { uint256 delegatedStake; uint256 selfStake; } diff --git a/contracts/interfaces/staking/INativeStakingReward.sol b/contracts/interfaces/staking/INativeStakingReward.sol new file mode 100644 index 0000000..ee81648 --- /dev/null +++ b/contracts/interfaces/staking/INativeStakingReward.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +interface INativeStakingReward { + function update(address account, address _stakeToken, address _operator) external; +} \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index e69de29..1ded03b 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +interface IStakingManager { + function onJobCreation(uint256 jobId, address operator, address token, uint256 amountToLock) external; + + function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external; + + function submitProof(uint256 jobId, bytes calldata proof) external; + + function setStakingManager(address _stakingManager) external; +} \ No newline at end of file diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index d62d38e..826c751 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -interface ISymbioticStaking { +import {IKalypsoStaking} from "./IKalypsoStaking.sol"; + +interface ISymbioticStaking is IKalypsoStaking { // function stakeOf(address _operator, address _token) external view returns (uint256); struct SnapshotTxCountInfo { @@ -31,6 +33,12 @@ interface ISymbioticStaking { address rewardAddress; } + struct ConfirmedTimestamp { + uint256 capturedTimestamp; + uint256 rewardShare; // TODO + address transmitter; + } + // event OperatorSnapshotSubmitted // event VaultSnapshotSubmitted @@ -38,4 +46,7 @@ interface ISymbioticStaking { // event SlashResultSubmitted // event SubmissionCompleted + + /// @notice Returns the captureTimestamp of latest completed snapshot submission + function lastConfirmedTimestamp() external view returns (uint256); } diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index cf050e9..49cdc8d 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -15,10 +15,10 @@ contract JobManager { struct JobInfo { address operator; - address token; - uint256 lockedAmount; + address lockToken; + uint256 lockedAmount; // this will go to slasher if the proof is not submitted before deadline uint256 deadline; - // address transmitter; // this should be stored in StakingManager + address dataTransmitter; // } mapping(uint256 => JobInfo) public jobs; @@ -27,7 +27,7 @@ contract JobManager { // TODO: called only from Kalypso Protocol // TODO: create a job and record StakeData Transmitter who submitted capture timestamp - jobs[jobId] = JobInfo(operator, token, amountToLock, block.timestamp + JOB_DURATION); + // TODO: call creation function in StakingManager stakingManager.onJobCreation(jobId, operator, token, amountToLock); diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index b6b11b5..81aacf2 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -26,12 +26,19 @@ contract NativeStaking is EnumerableSet.AddressSet private tokenSet; EnumerableSet.AddressSet private operatorSet; - mapping(address operator => mapping(address token => OperatorStakeInfo stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount - mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public - userStakeInfo; + address public nativeStakingReward; + + mapping(address operator => mapping(address token => OperatorStake stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount + mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public userStakeInfo; + mapping(uint256 jobId => NativeStakingLock) public lockInfo; mapping(bytes4 sig => bool isSupported) private supportedSignatures; + struct NativeStakingLock { + address token; + uint256 amount; + } + modifier onlySupportedToken(address _token) { require(tokenSet.contains(_token), "Token not supported"); _; @@ -64,7 +71,6 @@ contract NativeStaking is } // Staker should be able to choose an Operator they want to stake into - // This should update StakingManger's state function stake(address _account, address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) @@ -76,6 +82,8 @@ contract NativeStaking is userStakeInfo[_account][_operator][_token] += _amount; operatorStakeInfo[_operator][_token].delegatedStake += _amount; + // TODO: NativeStakingReward + emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -122,7 +130,9 @@ contract NativeStaking is /*======================================== Getters ========================================*/ - function getStakeAmount(address _operator, address _token) external view returns (uint256) {} + function getPoolStake(address _operator, address _token) external view returns (uint256) { + return operatorStakeInfo[_operator][_token].delegatedStake; + } function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts) {} @@ -137,6 +147,16 @@ contract NativeStaking is return operatorStakeInfo[_operator][_token].delegatedStake; } + // TODO: deduct locked amount + function getDelegateStakeActive(address _operator, address _token) + external + view + onlySupportedToken(_token) + returns (uint256) + { + return operatorStakeInfo[_operator][_token].delegatedStake; + } + // Returns the list of tokenSet staked by the operator and the amounts function getDelegatedStakes(address _operator) external @@ -179,7 +199,7 @@ contract NativeStaking is onlySupportedToken(_token) returns (uint256) { - OperatorStakeInfo memory _stakeInfo = operatorStakeInfo[_operator][_token]; + OperatorStake memory _stakeInfo = operatorStakeInfo[_operator][_token]; return _stakeInfo.delegatedStake + _stakeInfo.selfStake; } @@ -191,7 +211,7 @@ contract NativeStaking is uint256 len = tokenSet.length(); for (uint256 i = 0; i < len; i++) { - OperatorStakeInfo memory _stakeInfo = operatorStakeInfo[_operator][tokenSet.at(i)]; + OperatorStake memory _stakeInfo = operatorStakeInfo[_operator][tokenSet.at(i)]; _tokens[i] = tokenSet.at(i); _amounts[i] = _stakeInfo.delegatedStake + _stakeInfo.selfStake; } @@ -227,13 +247,21 @@ contract NativeStaking is supportedSignatures[sig] = isSupported; } - // TODO: set staking manager - /*======================================== StakingManager ========================================*/ - function lockStake(address _operator, address _token, uint256 _amount) external { - // TODO: StakingManager only + function lockStake(uint256 jobId, address operator, address _token, uint256 _amount) external { + // TODO: only staking manager - // TODO: decide whether to move or to just store in the mapping + lockInfo[jobId] = NativeStakingLock(_token, _amount); + operatorStakeInfo[operator][_token].delegatedStake -= _amount; + + // INativeStakingReward(nativeStakingReward).update(address(0), _token, operator); } + function unlockStake(uint256 jobId, address operator, address _token, uint256 _amount) external { + // TODO: only staking manager + lockInfo[jobId].amount -= _amount; + operatorStakeInfo[operator][_token].delegatedStake += _amount; + + // TODO: NativeStakingReward update + } } diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index b962051..6d0fff0 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -28,6 +28,8 @@ contract NativeStakingReward is using SafeERC20 for IERC20; address public nativeStaking; + address public feeRewardToken; + address public inflationRewardToken; // reward is accrued per operator mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) rewards; @@ -80,8 +82,21 @@ contract NativeStakingReward is // INativeStaking(nativeStaking).unstake(token, amount); } - function claimStake(address token) public { - // INativeStaking(nativeStaking).claimStake(token); + function claimReward(address token) public { + + } + + function addReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) public { + // TODO: only native staking + + rewards[_stakeToken][_operator][_rewardToken] += _amount; + _update(address(0), _stakeToken, _operator, _rewardToken); + } + + function update(address account, address _stakeToken, address _operator) public { + // TODO: only native staking + _update(account, _stakeToken, _operator, feeRewardToken); + _update(account, _stakeToken, _operator, inflationRewardToken); } function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { @@ -90,7 +105,6 @@ contract NativeStakingReward is claimableRewards[account][_stakeToken][_operator][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = currentRewardPerToken; - } function _pendingReward(address account, address _stakeToken, address operator, address _rewardToken) internal view returns (uint256) { diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 22b5abb..c16bddf 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -33,10 +33,11 @@ contract StakingManager is mapping(address pool => PoolConfig config) private poolConfig; - mapping(uint256 jobId => mapping(address pool => uint256 poolLockAmounts)) private poolLockAmounts; // lock amount for each pool - mapping(uint256 jobId => LockInfo lockInfo) private lockInfo; // total lock amount and unlock timestamp + // TODO: getter for retreiving the each pool's lock amount + mapping(uint256 jobId => uint256 lockAmount) private lockInfo; // total lock amount and unlock timestamp uint256 unlockEpoch; + uint256 stakeDataTransmitterShare; struct PoolConfig { uint256 weight; @@ -45,13 +46,11 @@ contract StakingManager is } // operator, lockToken, lockAmount should be stored in JobManager - struct LockInfo { + struct StakeLockInfo { uint256 totalLockAmount; uint256 unlockTimestamp; } - // TODO: integration with Slasher - function initialize(address _admin) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); @@ -68,67 +67,48 @@ contract StakingManager is function onJobCreation(uint256 _jobId, address _operator, address token, uint256 _lockAmount) external { // TODO: only jobManager - // TODO: lock operator selfstake (check how much) + // TODO: lock selfstake, Native Staking delegated stake, Symbiotic Stake uint256 len = stakingPoolSet.length(); + for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled if(!IKalypsoStaking(pool).isSupportedToken(token)) continue; // skip if the token is not supported by the pool - uint256 poolStake = getPoolStake(pool, _operator, token); + uint256 poolStake = IKalypsoStaking(pool).getPoolStake(pool, token); uint256 minStake = poolConfig[pool].minStake; // skip if the pool stake is less than the minStake //? case when lockAmount > minStake? + uint256 lockAmount = 0; if(poolStake >= minStake) { - uint256 lockAmount = _calcLockAmount(poolStake, poolConfig[pool].weight); // TODO: need to check formula for calculation - // TODO: move fund from the pool (implement lockStake in each pool) - // TODO: SymbioticStaking will just have empty code in it - _lockPoolStake(_jobId, pool, _operator, _lockAmount); + uint256 poolLockAmount = _calcLockAmount(poolStake, pool); // TODO: lock amount will be fixed + lockAmount += poolLockAmount; + IKalypsoStaking(pool).lockStake(_jobId, _operator, token, poolLockAmount); } } } - function getPoolStake(address _pool, address _operator, address _token) internal view returns (uint256) { - return IKalypsoStaking(_pool).getStakeAmount(_operator, _token); - } - - function _lockPoolStake(uint256 _jobId, address _pool, address _operator, uint256 _amount) internal { - // TODO: skip if the pool has less than minimum stake + // TODO: fix this + function _calcLockAmount(uint256 amount, address pool) internal view returns (uint256) { + uint256 weight = poolConfig[pool].weight; - lockInfo[_jobId].totalLockAmount += _amount; - lockInfo[_jobId].unlockTimestamp = block.timestamp + unlockEpoch; + return (amount * weight) / 10000; } - function _unlockStake(uint256 _jobId) internal { - lockInfo[_jobId].totalLockAmount = 0; - lockInfo[_jobId].unlockTimestamp = 0; - - // TODO: send back fund - } + // TODO + // function getPoolStake(address _pool, address _operator, address _token) internal view returns (uint256) { + // return IKalypsoStaking(_pool).getStakeAmount(_operator, _token); + // } // called when job is completed to unlock the locked stakes function onJobCompletion(uint256 _jobId) external { // TODO: only jobManager // TODO: unlock the locked stakes - _unlockStake(_jobId); - } - - // when certain period has passed after the lock and no slash result is submitted, this can be unlocked - // unlocking the locked stake does not check if token is enabled - function unlockStake(uint256 _jobId) external { - uint256 len = stakingPoolSet.length(); - address pool; - - for(uint256 i = 0; i < len; i++) { - pool = stakingPoolSet.at(i); - - // unlock the stake - _unlockStake(_jobId); - } + // _unlockStake(_jobId); } /*======================================== Getters ========================================*/ @@ -147,9 +127,7 @@ contract StakingManager is /*======================================== Getter for Staking ========================================*/ - function _calcLockAmount(uint256 amount, uint256 weight) internal pure returns (uint256) { - return (amount * weight) / 10000; // TODO: need to check formula for calculation (probably be the share) - } + /*======================================== Admin ========================================*/ @@ -166,9 +144,6 @@ contract StakingManager is // TODO: onlyAdmin } - function setSlashingManager(address _slashingManager) external { - // TODO: only admin - } // TODO: integration with JobManager function setJobManager(address _jobManager) external { @@ -184,6 +159,27 @@ contract StakingManager is // TODO: check if the unlockEpoch is longer than the proofDeadline } + // when job is closed, the reward will be distributed based on the share + function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external { + // TODO: only admin + require(_pools.length == _shares.length, "Invalid Length"); + + uint256 sum = 0; + for(uint256 i = 0; i < _shares.length; i++) { + sum += _shares[i]; + } + sum += _transmitterShare; + + // as the weight is in percentage, the sum of the shares should be 10000 + // TODO: sum of enabled pools should be 10000 + require(sum == 10000, "Invalid Shares"); + + for(uint256 i = 0; i < _pools.length; i++) { + poolConfig[_pools[i]].weight = _shares[i]; + } + stakeDataTransmitterShare = _transmitterShare; + } + /*======================================== Override ========================================*/ function supportsInterface(bytes4 interfaceId) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index fc76713..1352ad5 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -2,18 +2,16 @@ pragma solidity ^0.8.26; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +// TODO: vault => token info should be updated by the admin contract SymbioticStaking is ISymbioticStaking { - // TODO: address Operator => address token => CheckPoints.Trace256 stakeAmount (Question: operators' stake amount is consolidated within same vault?) + using EnumerableSet for EnumerableSet.AddressSet; - // TODO: set SD uint256 SD; - - // TODO: set TC uint256 TC; - //? How to manage Vault lists? - bytes4 public constant OPERATOR_SNAPSHOT_MASK = 0x00000001; bytes4 public constant VAULT_SNAPSHOT_MASK = 0x00000010; bytes4 public constant SLASH_RESULT_MASK = 0x00000100; @@ -23,17 +21,37 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); + // TODO: redundant to L1 Data + EnumerableSet.AddressSet vaultSet; + EnumerableSet.AddressSet tokenSet; + mapping(address vault => address token) public vaultToToken; + mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token + + /* Symbiotic Data Transmission */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received - mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))operatorSnapshots; - mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake)))vaultSnapshots; + // TODO: discuss this later (problem of who submitted the partial txs) + mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshots; + mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshots; mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) slashResultDatas; // TODO: need to check actual slashing timestamp? - uint256[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received + ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received + + struct SymbioticStakingLock { + address token; + uint256 amount; + address transmitter; + } + + /* Staking */ + mapping(uint256 jobId => SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake + /*======================================== L1 to L2 Transmission ========================================*/ // Transmitter submits staking data snapshot // This should update StakingManger's state + + // TODO function submitOperatorSnapshot( uint256 _index, uint256 _numOfTxs, // number of total transactions @@ -118,6 +136,28 @@ contract SymbioticStaking is ISymbioticStaking { } } + /*======================================== Job Creation ========================================*/ + // TODO: check if delegatedStake also gets locked + function lockStake(uint256 _jobId, address _operator, address _token, uint256 _amount) external { + require(isSupportedToken(_token), "Token not supported"); + + // Store transmitter address to reward when job is closed + address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; + lockInfo[_jobId] = SymbioticStakingLock(_token, _amount, transmitter); + } + + // TODO: check if delegatedStake also gets unlocked + function unlockStake(uint256 _jobId, address _operator, address _token, uint256 _amount) external { + require(isSupportedToken(_token), "Token not supported"); + + // TODO: only staking manager + lockInfo[_jobId].amount -= _amount; + } + + function getPoolStake(address _operator, address _token) external view returns (uint256) { + return operatorSnapshots[_operator][_token][lastConfirmedTimestamp()]; + } + /*======================================== Helpers ========================================*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); @@ -172,7 +212,6 @@ contract SymbioticStaking is ISymbioticStaking { } } - function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; @@ -196,7 +235,9 @@ contract SymbioticStaking is ISymbioticStaking { } function _completeSubmission(uint256 _captureTimestamp) internal { - confirmedTimestamps.push(_captureTimestamp); + // TODO: calc `TC` based on last submission + ConfirmedTimestamp memory confirmedTimestamp = ConfirmedTimestamp(_captureTimestamp, block.timestamp, msg.sender); + confirmedTimestamps.push(confirmedTimestamp); // TODO: calculate rewards for the transmitter based on TC // TODO: Data transmitter should get TC% of the rewards @@ -206,7 +247,26 @@ contract SymbioticStaking is ISymbioticStaking { } /*======================================== Getters ========================================*/ + + // TODO: remove + function getVaultToken(address _vault) public view returns (address) { + return vaultToToken[_vault]; + } + function lastConfirmedTimestamp() public view returns (uint256) { - return confirmedTimestamps[confirmedTimestamps.length - 1]; + return confirmedTimestamps[confirmedTimestamps.length - 1].capturedTimestamp; + } + + function isSupportedToken(address _token) public view returns (bool) { + // TODO + } + + function isSupportedVault(address _vault) public view returns (bool) { + // TODO + } + + /*======================================== Admin ========================================*/ + function setSupportedToken(address _token, bool _isSupported) external { + // TODO } } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index f7fc2f7..9002acf 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -50,6 +50,8 @@ contract SymbioticStakingReward is // notice: the total amount can be reduced when a job is created and the stake is locked mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public totalStakeAmounts; + + // TODO: this should be pulled from SymbioticStaking contract // locked amount for each stakeToken upon job creation mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public lockedStakeAmounts; @@ -98,7 +100,7 @@ contract SymbioticStakingReward is } //-------------------------------- Init end --------------------------------// - //-------------------------------- StakingManager start --------------------------------// + //-------------------------------- SymbioticStaking start --------------------------------// /// @notice updates stake amount of a given vault /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed /// @dev only can be called by SymbioticStaking contract @@ -188,7 +190,7 @@ contract SymbioticStakingReward is return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); } - //-------------------------------- StakingManager end --------------------------------// + //-------------------------------- SymbioticStaking end --------------------------------// //-------------------------------- Update start --------------------------------// From 230b8ba7ca1127e3e8deb24bceb192be2701a90f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 18 Sep 2024 18:26:52 +0800 Subject: [PATCH 050/158] fix signature for lockStake --- .../interfaces/staking/IKalypsoStaking.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 6 +- .../staking/l2_contracts/StakingExample.sol | 144 ++++++++++++++++++ .../staking/l2_contracts/StakingManager.sol | 8 +- .../staking/l2_contracts/SymbioticStaking.sol | 20 +-- 5 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 contracts/staking/l2_contracts/StakingExample.sol diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index d30c78f..7792f1f 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -6,7 +6,7 @@ interface IKalypsoStaking { function getPoolStake(address _operator, address _token) external view returns (uint256); - function lockStake(uint256 jobId, address operator, address _token, uint256 _amount) external; // Staking Manager only + function lockStake(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external; // Staking Manager only struct PoolLockInfo { address token; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 81aacf2..e6ef026 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -248,11 +248,11 @@ contract NativeStaking is } /*======================================== StakingManager ========================================*/ - function lockStake(uint256 jobId, address operator, address _token, uint256 _amount) external { + function lockStake(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external { // TODO: only staking manager - lockInfo[jobId] = NativeStakingLock(_token, _amount); - operatorStakeInfo[operator][_token].delegatedStake -= _amount; + lockInfo[_jobId] = NativeStakingLock(_token, _delegatedStakeLock); + operatorStakeInfo[_operator][_token].delegatedStake -= _delegatedStakeLock; // INativeStakingReward(nativeStakingReward).update(address(0), _token, operator); } diff --git a/contracts/staking/l2_contracts/StakingExample.sol b/contracts/staking/l2_contracts/StakingExample.sol new file mode 100644 index 0000000..1dcf5d8 --- /dev/null +++ b/contracts/staking/l2_contracts/StakingExample.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract StakingRewards { + IERC20 public immutable stakingToken; + IERC20 public immutable rewardsToken; + + address public owner; + + // Duration of rewards to be paid out (in seconds) + uint256 public duration; + // Timestamp of when the rewards finish + uint256 public finishAt; + // Minimum of last updated time and reward finish time + uint256 public updatedAt; + // Reward to be paid out per second + uint256 public rewardRate; + // Sum of (reward rate * dt * 1e18 / total supply) + uint256 public rewardPerTokenStored; + // User address => rewardPerTokenStored + mapping(address => uint256) public userRewardPerTokenPaid; + // User address => rewards to be claimed + mapping(address => uint256) public rewards; + + // Total staked + uint256 public totalSupply; + // User address => staked amount + mapping(address => uint256) public balanceOf; + + constructor(address _stakingToken, address _rewardToken) { + owner = msg.sender; + stakingToken = IERC20(_stakingToken); + rewardsToken = IERC20(_rewardToken); + } + + modifier onlyOwner() { + require(msg.sender == owner, "not authorized"); + _; + } + + modifier updateReward(address _account) { + rewardPerTokenStored = rewardPerToken(); + updatedAt = lastTimeRewardApplicable(); + + if (_account != address(0)) { + rewards[_account] = earned(_account); + userRewardPerTokenPaid[_account] = rewardPerTokenStored; + } + + _; + } + + function lastTimeRewardApplicable() public view returns (uint256) { + return _min(finishAt, block.timestamp); + } + + function rewardPerToken() public view returns (uint256) { + if (totalSupply == 0) { + return rewardPerTokenStored; + } + + return rewardPerTokenStored + + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) + / totalSupply; + } + + function stake(uint256 _amount) external updateReward(msg.sender) { + require(_amount > 0, "amount = 0"); + stakingToken.transferFrom(msg.sender, address(this), _amount); + balanceOf[msg.sender] += _amount; + totalSupply += _amount; + } + + function withdraw(uint256 _amount) external updateReward(msg.sender) { + require(_amount > 0, "amount = 0"); + balanceOf[msg.sender] -= _amount; + totalSupply -= _amount; + stakingToken.transfer(msg.sender, _amount); + } + + function earned(address _account) public view returns (uint256) { + return ( + ( + balanceOf[_account] + * (rewardPerToken() - userRewardPerTokenPaid[_account]) + ) / 1e18 + ) + rewards[_account]; + } + + function getReward() external updateReward(msg.sender) { + uint256 reward = rewards[msg.sender]; + if (reward > 0) { + rewards[msg.sender] = 0; + rewardsToken.transfer(msg.sender, reward); + } + } + + function setRewardsDuration(uint256 _duration) external onlyOwner { + require(finishAt < block.timestamp, "reward duration not finished"); + duration = _duration; + } + + function notifyRewardAmount(uint256 _amount) + external + onlyOwner + updateReward(address(0)) + { + if (block.timestamp >= finishAt) { + rewardRate = _amount / duration; + } else { + uint256 remainingRewards = (finishAt - block.timestamp) * rewardRate; + rewardRate = (_amount + remainingRewards) / duration; + } + + require(rewardRate > 0, "reward rate = 0"); + require( + rewardRate * duration <= rewardsToken.balanceOf(address(this)), + "reward amount > balance" + ); + + finishAt = block.timestamp + duration; + updatedAt = block.timestamp; + } + + function _min(uint256 x, uint256 y) private pure returns (uint256) { + return x <= y ? x : y; + } +} + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) + external + returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) + external + returns (bool); +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index c16bddf..8148aae 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -64,7 +64,7 @@ contract StakingManager is // locked stake will be unlocked after an epoch if no slas result is submitted // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) - function onJobCreation(uint256 _jobId, address _operator, address token, uint256 _lockAmount) external { + function onJobCreation(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external { // TODO: only jobManager // TODO: lock selfstake, Native Staking delegated stake, Symbiotic Stake @@ -75,9 +75,9 @@ contract StakingManager is address pool = stakingPoolSet.at(i); if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled - if(!IKalypsoStaking(pool).isSupportedToken(token)) continue; // skip if the token is not supported by the pool + if(!IKalypsoStaking(pool).isSupportedToken(_token)) continue; // skip if the token is not supported by the pool - uint256 poolStake = IKalypsoStaking(pool).getPoolStake(pool, token); + uint256 poolStake = IKalypsoStaking(pool).getPoolStake(pool, _token); uint256 minStake = poolConfig[pool].minStake; // skip if the pool stake is less than the minStake @@ -86,7 +86,7 @@ contract StakingManager is if(poolStake >= minStake) { uint256 poolLockAmount = _calcLockAmount(poolStake, pool); // TODO: lock amount will be fixed lockAmount += poolLockAmount; - IKalypsoStaking(pool).lockStake(_jobId, _operator, token, poolLockAmount); + IKalypsoStaking(pool).lockStake(_jobId, _operator, _token, _selfStakeLock, _delegatedStakeLock); } } } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 1352ad5..0340f30 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -32,9 +32,9 @@ contract SymbioticStaking is ISymbioticStaking { mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received // TODO: discuss this later (problem of who submitted the partial txs) - mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorSnapshots; - mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultSnapshots; - mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult SlashResultData)) slashResultDatas; // TODO: need to check actual slashing timestamp? + mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorStakes; + mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultStakes; + mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult slashResult)) slashResults; // TODO: need to check actual slashing timestamp? ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received @@ -138,12 +138,12 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked - function lockStake(uint256 _jobId, address _operator, address _token, uint256 _amount) external { + function lockStake(uint256 _jobId, address _operator, address _token, uint256 _delegatedStakeLock, uint256 /* selfStakeLock */) external { require(isSupportedToken(_token), "Token not supported"); // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - lockInfo[_jobId] = SymbioticStakingLock(_token, _amount, transmitter); + lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); } // TODO: check if delegatedStake also gets unlocked @@ -155,8 +155,8 @@ contract SymbioticStaking is ISymbioticStaking { } function getPoolStake(address _operator, address _token) external view returns (uint256) { - return operatorSnapshots[_operator][_token][lastConfirmedTimestamp()]; - } + return operatorStakes[_operator][_token][lastConfirmedTimestamp()]; + } /*======================================== Helpers ========================================*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { @@ -205,7 +205,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _operatorSnapshots.length; i++) { OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; - operatorSnapshots[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = + operatorStakes[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = _operatorSnapshot.stake; // TODO: emit event for each update? @@ -216,7 +216,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; - vaultSnapshots[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; + vaultStakes[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; // TODO: emit event for each update? } @@ -228,7 +228,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _SlashResultDatas.length; i++) { SlashResultData memory _slashResultData = _SlashResultDatas[i]; - slashResultDatas[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; + slashResults[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; // TODO: emit event for each update? } From dfc1a8b55a9db457515b4a8ed80ca8c22021eec5 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 18 Sep 2024 19:11:45 +0800 Subject: [PATCH 051/158] fix jobCreation logic --- .../interfaces/staking/IKalypsoStaking.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 55 +++++++------------ .../staking/l2_contracts/StakingManager.sol | 4 +- .../staking/l2_contracts/SymbioticStaking.sol | 2 +- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index 7792f1f..601d762 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -6,7 +6,7 @@ interface IKalypsoStaking { function getPoolStake(address _operator, address _token) external view returns (uint256); - function lockStake(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external; // Staking Manager only + function lockStake(uint256 _jobId, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external; // Staking Manager only struct PoolLockInfo { address token; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index e6ef026..c0d48b4 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -36,7 +36,8 @@ contract NativeStaking is struct NativeStakingLock { address token; - uint256 amount; + uint256 delegatedStake; + uint256 selfStake; } modifier onlySupportedToken(address _token) { @@ -158,18 +159,18 @@ contract NativeStaking is } // Returns the list of tokenSet staked by the operator and the amounts - function getDelegatedStakes(address _operator) - external - view - returns (address[] memory _tokens, uint256[] memory _amounts) - { - uint256 len = tokenSet.length(); - - for (uint256 i = 0; i < len; i++) { - _tokens[i] = tokenSet.at(i); - _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].delegatedStake; - } - } + // function getDelegatedStakes(address _operator) + // external + // view + // returns (address[] memory _tokens, uint256[] memory _amounts) + // { + // uint256 len = tokenSet.length(); + + // for (uint256 i = 0; i < len; i++) { + // _tokens[i] = tokenSet.at(i); + // _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].delegatedStake; + // } + // } function getSelfStake(address _operator, address _token) external @@ -180,18 +181,6 @@ contract NativeStaking is return operatorStakeInfo[_operator][_token].selfStake; } - function getSelfStakes(address _operator) - external - view - returns (address[] memory _tokens, uint256[] memory _amounts) - { - uint256 len = tokenSet.length(); - - for (uint256 i = 0; i < len; i++) { - _tokens[i] = tokenSet.at(i); - _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].selfStake; - } - } function getOperatorTotalStake(address _operator, address _token) external @@ -248,20 +237,18 @@ contract NativeStaking is } /*======================================== StakingManager ========================================*/ - function lockStake(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external { + function lockStake(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 _selfStakeLock) external { // TODO: only staking manager - lockInfo[_jobId] = NativeStakingLock(_token, _delegatedStakeLock); - operatorStakeInfo[_operator][_token].delegatedStake -= _delegatedStakeLock; - - // INativeStakingReward(nativeStakingReward).update(address(0), _token, operator); + lockInfo[_jobId] = NativeStakingLock(_token, _delegatedStakeLock, _selfStakeLock); + // TODO: emit event } - function unlockStake(uint256 jobId, address operator, address _token, uint256 _amount) external { + function unlockStake(uint256 _jobId) external { // TODO: only staking manager - lockInfo[jobId].amount -= _amount; - operatorStakeInfo[operator][_token].delegatedStake += _amount; - // TODO: NativeStakingReward update + lockInfo[_jobId] = NativeStakingLock(address(0), 0, 0); + + // TODO: emit event } } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 8148aae..8884afb 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -64,7 +64,7 @@ contract StakingManager is // locked stake will be unlocked after an epoch if no slas result is submitted // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) - function onJobCreation(uint256 _jobId, address _operator, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external { + function onJobCreation(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 _selfStakeLock) external { // TODO: only jobManager // TODO: lock selfstake, Native Staking delegated stake, Symbiotic Stake @@ -86,7 +86,7 @@ contract StakingManager is if(poolStake >= minStake) { uint256 poolLockAmount = _calcLockAmount(poolStake, pool); // TODO: lock amount will be fixed lockAmount += poolLockAmount; - IKalypsoStaking(pool).lockStake(_jobId, _operator, _token, _selfStakeLock, _delegatedStakeLock); + IKalypsoStaking(pool).lockStake(_jobId, _token, _selfStakeLock, _delegatedStakeLock); } } } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 0340f30..3194589 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -138,7 +138,7 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked - function lockStake(uint256 _jobId, address _operator, address _token, uint256 _delegatedStakeLock, uint256 /* selfStakeLock */) external { + function lockStake(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 /* selfStakeLock */) external { require(isSupportedToken(_token), "Token not supported"); // Store transmitter address to reward when job is closed From 97d4152ba741672a62d1b6aaf61278da58ed8f54 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 18 Sep 2024 19:23:17 +0800 Subject: [PATCH 052/158] add unlock logic --- contracts/interfaces/staking/IKalypsoStaking.sol | 2 ++ contracts/interfaces/staking/IStakingManager.sol | 2 ++ contracts/staking/l2_contracts/JobManager.sol | 8 +++++++- contracts/staking/l2_contracts/StakingManager.sol | 11 +++++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index 601d762..767b7ea 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -8,6 +8,8 @@ interface IKalypsoStaking { function lockStake(uint256 _jobId, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external; // Staking Manager only + function unlockStake(uint256 _jobId) external; // Staking Manager only + struct PoolLockInfo { address token; uint256 amount; diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 1ded03b..fa722c4 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator, address token, uint256 amountToLock) external; + function onJobCompletion(uint256 jobId, address token) external; + function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external; function submitProof(uint256 jobId, bytes calldata proof) external; diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 49cdc8d..e487f02 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -36,7 +36,7 @@ contract JobManager { /** * @notice Submit Multiple proofs in single transaction */ - function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external { + function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external { require(jobIds.length == proofs.length, "Invalid Length"); for (uint256 index = 0; index < jobIds.length; index++) { @@ -44,6 +44,12 @@ contract JobManager { } // TODO: close job and distribute rewards + uint256 len = jobIds.length; + for (uint256 i = 0; i < len; i++) { + uint256 jobId; + stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); + } + } /** diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 8884afb..843a02f 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -104,11 +104,18 @@ contract StakingManager is // } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId) external { + function onJobCompletion(uint256 _jobId, address _token) external { // TODO: only jobManager // TODO: unlock the locked stakes - // _unlockStake(_jobId); + uint256 len = stakingPoolSet.length(); + for(uint256 i = 0; i < len; i++) { + address pool = stakingPoolSet.at(i); + + IKalypsoStaking(pool).unlockStake(_jobId); + } + + } /*======================================== Getters ========================================*/ From 7dab57463b081de2741f1a92edd58ff84c00362a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 18 Sep 2024 19:26:09 +0800 Subject: [PATCH 053/158] resolve cimpile error --- contracts/staking/l2_contracts/SymbioticStaking.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 3194589..c2dbe31 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -144,14 +144,16 @@ contract SymbioticStaking is ISymbioticStaking { // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); + + // TODO: emit event } // TODO: check if delegatedStake also gets unlocked - function unlockStake(uint256 _jobId, address _operator, address _token, uint256 _amount) external { - require(isSupportedToken(_token), "Token not supported"); - + function unlockStake(uint256 _jobId) external { // TODO: only staking manager - lockInfo[_jobId].amount -= _amount; + lockInfo[_jobId].amount = 0; + + // TODO: emit event } function getPoolStake(address _operator, address _token) external view returns (uint256) { From 842c686d09bbef79f0715ee81acd900c9cecb9cc Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 19 Sep 2024 13:51:53 +0800 Subject: [PATCH 054/158] fix NativeStaking stake logic --- contracts/staking/l2_contracts/JobManager.sol | 4 ++-- .../staking/l2_contracts/NativeStaking.sol | 5 ++++- .../l2_contracts/NativeStakingReward.sol | 18 +++--------------- .../staking/l2_contracts/StakingManager.sol | 4 ++-- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index e487f02..0e8ccb2 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -30,7 +30,7 @@ contract JobManager { // TODO: call creation function in StakingManager - stakingManager.onJobCreation(jobId, operator, token, amountToLock); + stakingManager.onJobCreation(jobId, operator, token, amountToLock); // lock stake } /** @@ -47,7 +47,7 @@ contract JobManager { uint256 len = jobIds.length; for (uint256 i = 0; i < len; i++) { uint256 jobId; - stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); + stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake } } diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index c0d48b4..0f62ddb 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -11,6 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; +import {INativeStakingReward} from "../../interfaces/staking/INativeStakingReward.sol"; contract NativeStaking is ContextUpgradeable, @@ -83,7 +84,9 @@ contract NativeStaking is userStakeInfo[_account][_operator][_token] += _amount; operatorStakeInfo[_operator][_token].delegatedStake += _amount; - // TODO: NativeStakingReward + // NativeStakingReward contract will read staking amount info from this contract + // and update reward related states + INativeStakingReward(nativeStakingReward).update(_account, _token, _operator); emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 6d0fff0..fc7d7b4 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -66,21 +66,7 @@ contract NativeStakingReward is //-------------------------------- NativeStaking start --------------------------------// - function stake(address token, uint256 amount) public { - // INativeStaking(nativeStaking).stake(token, amount); - } - - function lockStake(address token, uint256 amount, uint256 lockDuration) public { - // INativeStaking(nativeStaking).lockStake(token, amount, lockDuration); - } - function unlockStake(address token, uint256 amount) public { - // INativeStaking(nativeStaking).unlockStake(token, amount); - } - - function unstake(address token, uint256 amount) public { - // INativeStaking(nativeStaking).unstake(token, amount); - } function claimReward(address token) public { @@ -92,7 +78,9 @@ contract NativeStakingReward is rewards[_stakeToken][_operator][_rewardToken] += _amount; _update(address(0), _stakeToken, _operator, _rewardToken); } - + + /// @notice pulls stake amount info from NativeStaking and updates + /// @notice stake amount will be tracked in NativeStaking function update(address account, address _stakeToken, address _operator) public { // TODO: only native staking _update(account, _stakeToken, _operator, feeRewardToken); diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 843a02f..451869f 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -104,7 +104,7 @@ contract StakingManager is // } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId, address _token) external { + function onJobCompletion(uint256 _jobId) external { // TODO: only jobManager // TODO: unlock the locked stakes @@ -115,7 +115,7 @@ contract StakingManager is IKalypsoStaking(pool).unlockStake(_jobId); } - + // TODO: emit event } /*======================================== Getters ========================================*/ From 10d3ab6940c65ad6e7d5adc30cf537d55dba6000 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 19 Sep 2024 13:53:20 +0800 Subject: [PATCH 055/158] hardhat config for compile --- hardhat.config.ts | 153 +++++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 71 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 700945e..b031394 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,14 +1,15 @@ -import { HardhatUserConfig, task } from "hardhat/config"; -import "@nomicfoundation/hardhat-toolbox"; -import "@openzeppelin/hardhat-upgrades"; -import "@nomicfoundation/hardhat-chai-matchers"; +import '@nomicfoundation/hardhat-toolbox'; +import '@openzeppelin/hardhat-upgrades'; +import '@nomicfoundation/hardhat-chai-matchers'; +import 'hardhat-gas-reporter'; +import 'solidity-coverage'; -import "hardhat-gas-reporter"; -import "solidity-coverage"; - -import { config as dotenvConfig } from "dotenv"; - -import BigNumber from "bignumber.js"; +import BigNumber from 'bignumber.js'; +import { config as dotenvConfig } from 'dotenv'; +import { + HardhatUserConfig, + task, +} from 'hardhat/config'; dotenvConfig(); @@ -24,6 +25,16 @@ task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { const config: HardhatUserConfig = { solidity: { compilers: [ + { + version: "0.8.26", + settings: { + viaIR: true, + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, { version: "0.8.24", settings: { @@ -78,67 +89,67 @@ const config: HardhatUserConfig = { hardhat: { blockGasLimit: 500000000000, }, - sepolia: { - url: `${process.env.SEPOLIA_RPC_URL}`, - // NOTE: don't change the order of elements in the array, add new elements at the last. - accounts: [ - `${process.env.SEPOLIA_ADMIN}`, - `${process.env.SEPOLIA_TOKEN_HOLDER}`, - `${process.env.SEPOLIA_TREASURY}`, - `${process.env.SEPOLIA_MARKET_CREATOR}`, - `${process.env.SEPOLIA_GENERATOR}`, - `${process.env.SEPOLIA_MATCHING_ENGINE}`, - `${process.env.SEPOLIA_PROOF_REQUESTOR}`, - ], - }, - arbSepolia: { - url: `${process.env.ARB_SEPOLIA_RPC_URL}`, - accounts: [ - `${process.env.SEPOLIA_ADMIN}`, - `${process.env.SEPOLIA_TOKEN_HOLDER}`, - `${process.env.SEPOLIA_TREASURY}`, - `${process.env.SEPOLIA_MARKET_CREATOR}`, - `${process.env.SEPOLIA_GENERATOR}`, - `${process.env.SEPOLIA_MATCHING_ENGINE}`, - `${process.env.SEPOLIA_PROOF_REQUESTOR}`, - ], - }, - nova: { - url: `${process.env.NOVA_RPC_URL}`, - accounts: [ - `${process.env.NOVA_ADMIN}`, - `${process.env.NOVA_TOKEN_HOLDER}`, - `${process.env.NOVA_TREASURY}`, - `${process.env.NOVA_MARKET_CREATOR}`, - `${process.env.NOVA_GENERATOR}`, - `${process.env.NOVA_MATCHING_ENGINE}`, - `${process.env.NOVA_PROOF_REQUESTOR}`, - ], - }, - zksync: { - url: `${process.env.ZKSYNC_URL}`, - accounts: [ - `${process.env.ZKSYNC_ADMIN}`, - `${process.env.ZKSYNC_TOKEN_HOLDER}`, - `${process.env.ZKSYNC_TREASURY}`, - `${process.env.ZKSYNC_MARKET_CREATOR}`, - `${process.env.ZKSYNC_GENERATOR}`, - `${process.env.ZKSYNC_MATCHING_ENGINE}`, - `${process.env.ZKSYNC_PROOF_REQUESTOR}`, - ], - }, - amoy: { - url: `${process.env.AMOY_RPC}`, - accounts: [ - `${process.env.AMOY_ADMIN}`, - `${process.env.AMOY_TOKEN_HOLDER}`, - `${process.env.AMOY_TREASURY}`, - `${process.env.AMOY_MARKET_CREATOR}`, - `${process.env.AMOY_GENERATOR}`, - `${process.env.AMOY_MATCHING_ENGINE}`, - `${process.env.AMOY_PROOF_REQUESTOR}`, - ], - }, + // sepolia: { + // url: `${process.env.SEPOLIA_RPC_URL}`, + // // NOTE: don't change the order of elements in the array, add new elements at the last. + // accounts: [ + // `${process.env.SEPOLIA_ADMIN}`, + // `${process.env.SEPOLIA_TOKEN_HOLDER}`, + // `${process.env.SEPOLIA_TREASURY}`, + // `${process.env.SEPOLIA_MARKET_CREATOR}`, + // `${process.env.SEPOLIA_GENERATOR}`, + // `${process.env.SEPOLIA_MATCHING_ENGINE}`, + // `${process.env.SEPOLIA_PROOF_REQUESTOR}`, + // ], + // }, + // arbSepolia: { + // url: `${process.env.ARB_SEPOLIA_RPC_URL}`, + // accounts: [ + // `${process.env.SEPOLIA_ADMIN}`, + // `${process.env.SEPOLIA_TOKEN_HOLDER}`, + // `${process.env.SEPOLIA_TREASURY}`, + // `${process.env.SEPOLIA_MARKET_CREATOR}`, + // `${process.env.SEPOLIA_GENERATOR}`, + // `${process.env.SEPOLIA_MATCHING_ENGINE}`, + // `${process.env.SEPOLIA_PROOF_REQUESTOR}`, + // ], + // }, + // nova: { + // url: `${process.env.NOVA_RPC_URL}`, + // accounts: [ + // `${process.env.NOVA_ADMIN}`, + // `${process.env.NOVA_TOKEN_HOLDER}`, + // `${process.env.NOVA_TREASURY}`, + // `${process.env.NOVA_MARKET_CREATOR}`, + // `${process.env.NOVA_GENERATOR}`, + // `${process.env.NOVA_MATCHING_ENGINE}`, + // `${process.env.NOVA_PROOF_REQUESTOR}`, + // ], + // }, + // zksync: { + // url: `${process.env.ZKSYNC_URL}`, + // accounts: [ + // `${process.env.ZKSYNC_ADMIN}`, + // `${process.env.ZKSYNC_TOKEN_HOLDER}`, + // `${process.env.ZKSYNC_TREASURY}`, + // `${process.env.ZKSYNC_MARKET_CREATOR}`, + // `${process.env.ZKSYNC_GENERATOR}`, + // `${process.env.ZKSYNC_MATCHING_ENGINE}`, + // `${process.env.ZKSYNC_PROOF_REQUESTOR}`, + // ], + // }, + // amoy: { + // url: `${process.env.AMOY_RPC}`, + // accounts: [ + // `${process.env.AMOY_ADMIN}`, + // `${process.env.AMOY_TOKEN_HOLDER}`, + // `${process.env.AMOY_TREASURY}`, + // `${process.env.AMOY_MARKET_CREATOR}`, + // `${process.env.AMOY_GENERATOR}`, + // `${process.env.AMOY_MATCHING_ENGINE}`, + // `${process.env.AMOY_PROOF_REQUESTOR}`, + // ], + // }, }, }; From 3cc69148425ef7b5059264157da981c52c0d5051 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 19 Sep 2024 13:58:13 +0800 Subject: [PATCH 056/158] fix withdrawStake logic --- contracts/staking/l2_contracts/NativeStaking.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 0f62ddb..04a7cc0 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -107,19 +107,21 @@ contract NativeStaking is } // This should update StakingManger's state - function withdrawStake(address operator, address token, uint256 amount) external nonReentrant { - require(userStakeInfo[msg.sender][operator][token] >= amount, "Insufficient stake"); + function withdrawStake(address _account, address _operator, address _token, uint256 _amount) external nonReentrant { + require(userStakeInfo[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); // TODO: check locked time // TODO: read from staking manager and calculate withdrawable amount - IERC20(token).safeTransfer(msg.sender, amount); + IERC20(_token).safeTransfer(msg.sender, _amount); - userStakeInfo[msg.sender][operator][token] -= amount; - operatorStakeInfo[operator][token].delegatedStake -= amount; + userStakeInfo[msg.sender][_operator][_token] -= _amount; + operatorStakeInfo[_operator][_token].delegatedStake -= _amount; - emit StakeWithdrawn(msg.sender, operator, token, amount, block.timestamp); + INativeStakingReward(nativeStakingReward).update(_account, _token, _operator); + + emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); } function withdrawSelfStake(address operator, address token, uint256 amount) external nonReentrant { From 5bb69e678465713f50f478f7175c7ad238f2a74e Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 23 Sep 2024 17:40:33 +0900 Subject: [PATCH 057/158] reflect review comments --- contracts/interfaces/staking/ISymbioticStaking.sol | 3 ++- contracts/staking/l2_contracts/JobManager.sol | 9 +++++---- contracts/staking/l2_contracts/StakingManager.sol | 7 ++----- contracts/staking/l2_contracts/SymbioticStaking.sol | 7 ++++++- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 826c751..0e95e0a 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -16,7 +16,8 @@ interface ISymbioticStaking is IKalypsoStaking { address token; uint256 stake; } - + + //! TODO: add operator struct VaultSnapshot { address vault; address token; diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 0e8ccb2..877218f 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -23,6 +23,7 @@ contract JobManager { mapping(uint256 => JobInfo) public jobs; + // TODO: token => lockAmount logic function createJob(uint256 jobId, address operator, address token, uint256 amountToLock) external { // TODO: called only from Kalypso Protocol @@ -39,13 +40,13 @@ contract JobManager { function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external { require(jobIds.length == proofs.length, "Invalid Length"); - for (uint256 index = 0; index < jobIds.length; index++) { - _verifyProof(jobIds[index], proofs[index]); - } // TODO: close job and distribute rewards uint256 len = jobIds.length; - for (uint256 i = 0; i < len; i++) { + + for (uint256 idx = 0; idx < len; idx++) { + _verifyProof(jobIds[idx], proofs[idx]); + uint256 jobId; stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 451869f..66503a6 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -74,6 +74,7 @@ contract StakingManager is for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); + //! TODO: check github comments if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled if(!IKalypsoStaking(pool).isSupportedToken(_token)) continue; // skip if the token is not supported by the pool @@ -91,6 +92,7 @@ contract StakingManager is } } + //! TODO: remove this and read from mapping // TODO: fix this function _calcLockAmount(uint256 amount, address pool) internal view returns (uint256) { uint256 weight = poolConfig[pool].weight; @@ -157,11 +159,6 @@ contract StakingManager is // TODO: only admin } - // TODO: interaction with Price Oracle - function setPriceOracle(address _priceOracle) external { - // TODO - } - function setUnlockEpoch(uint256 _unlockEpoch) external { // TODO: check if the unlockEpoch is longer than the proofDeadline } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index c2dbe31..e384388 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -9,9 +9,11 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet contract SymbioticStaking is ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; + //! TODO: make variable more descriptive uint256 SD; uint256 TC; + //! TODO: bytes32 bytes4 public constant OPERATOR_SNAPSHOT_MASK = 0x00000001; bytes4 public constant VAULT_SNAPSHOT_MASK = 0x00000010; bytes4 public constant SLASH_RESULT_MASK = 0x00000100; @@ -22,7 +24,8 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); // TODO: redundant to L1 Data - EnumerableSet.AddressSet vaultSet; + //! TODO: check if needed + EnumerableSet.AddressSet vaultSet; EnumerableSet.AddressSet tokenSet; mapping(address vault => address token) public vaultToToken; mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token @@ -134,6 +137,8 @@ contract SymbioticStaking is ISymbioticStaking { if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); } + + // TODO: unlock the selfStake and reward it to the transmitter } /*======================================== Job Creation ========================================*/ From a46460673d6a2cdc413b604479e08642d454e4f0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 23 Sep 2024 18:03:06 +0900 Subject: [PATCH 058/158] fix mask from bytes4 to bytes32 --- .../staking/l2_contracts/SymbioticStaking.sol | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index e384388..c00ab8f 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -13,26 +13,23 @@ contract SymbioticStaking is ISymbioticStaking { uint256 SD; uint256 TC; - //! TODO: bytes32 - bytes4 public constant OPERATOR_SNAPSHOT_MASK = 0x00000001; - bytes4 public constant VAULT_SNAPSHOT_MASK = 0x00000010; - bytes4 public constant SLASH_RESULT_MASK = 0x00000100; - bytes4 public constant COMPLETE_MASK = 0x00000111; + bytes32 public constant OPERATOR_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; + bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000100; + bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000111; bytes32 public constant OPERATOR_SNAPSHOT = keccak256("OPERATOR_SNAPSHOT"); bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); // TODO: redundant to L1 Data - //! TODO: check if needed - EnumerableSet.AddressSet vaultSet; EnumerableSet.AddressSet tokenSet; mapping(address vault => address token) public vaultToToken; mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token /* Symbiotic Data Transmission */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received - mapping(uint256 captureTimestamp => mapping(address account => bytes4 status)) submissionStatus; // to check if all partial txs are received + mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received // TODO: discuss this later (problem of who submitted the partial txs) mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorStakes; @@ -176,7 +173,7 @@ contract SymbioticStaking is ISymbioticStaking { require(snapshot.count < snapshot.numOfTxs, "Snapshot fully submitted already"); require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); - bytes4 mask; + bytes32 mask; if (_type == OPERATOR_SNAPSHOT) mask = OPERATOR_SNAPSHOT_MASK; else if (_type == VAULT_SNAPSHOT) mask = VAULT_SNAPSHOT_MASK; else if (_type == SLASH_RESULT) mask = SLASH_RESULT_MASK; From fe4a8dc97b49ea8005890f90aa07350143ce63a7 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 24 Sep 2024 13:40:48 +0900 Subject: [PATCH 059/158] move lockStake logic from StsakingManager to NativeStaking --- .../interfaces/staking/IKalypsoStaking.sol | 4 +- .../interfaces/staking/INativeStaking.sol | 4 - .../staking/l2_contracts/NativeStaking.sol | 157 +++++++----------- .../staking/l2_contracts/StakingManager.sol | 37 +---- .../staking/l2_contracts/SymbioticStaking.sol | 14 +- 5 files changed, 77 insertions(+), 139 deletions(-) diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IKalypsoStaking.sol index 767b7ea..bbca976 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IKalypsoStaking.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.26; interface IKalypsoStaking { function isSupportedToken(address _token) external view returns (bool); - function getPoolStake(address _operator, address _token) external view returns (uint256); + // function getPoolStake(address _operator, address _token) external view returns (uint256); - function lockStake(uint256 _jobId, address _token, uint256 _selfStakeLock, uint256 _delegatedStakeLock) external; // Staking Manager only + function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only function unlockStake(uint256 _jobId) external; // Staking Manager only diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 071f81b..4cb8b89 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,10 +4,6 @@ pragma solidity ^0.8.26; import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; interface INativeStaking is IKalypsoStaking { - struct OperatorStake { - uint256 delegatedStake; - uint256 selfStake; - } // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 04a7cc0..5384593 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -25,20 +25,34 @@ contract NativeStaking is using SafeERC20 for IERC20; EnumerableSet.AddressSet private tokenSet; - EnumerableSet.AddressSet private operatorSet; + EnumerableSet.AddressSet private operatorSet; // TODO: check if needed address public nativeStakingReward; - mapping(address operator => mapping(address token => OperatorStake stakeInfo)) public operatorStakeInfo; // stakeAmount, selfStakeAmount - mapping(address account => mapping(address operator => mapping(address token => uint256 stake))) public userStakeInfo; - mapping(uint256 jobId => NativeStakingLock) public lockInfo; - + /*======================================== Config ========================================*/ + // config + mapping(address token => uint256 minStakeamount) public minStakeAmount; + mapping(address token => uint256 lockAmount) public amountToLock; mapping(bytes4 sig => bool isSupported) private supportedSignatures; + // stake + + // total staked amounts for each operator, includes selfStake and delegatedStake amount + mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; + // selfstake if account == operator + mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; + // total staked amount for each token + mapping(address token => uint256 amount) public totalStakedAmounts; + + // lock + // TODO: check if mappings below are needed + mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; + mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; // includes selfStake and delegatedStake amount + mapping(address token => uint256 amount) public totalLockedAmounts; + struct NativeStakingLock { address token; - uint256 delegatedStake; - uint256 selfStake; + uint256 amount; } modifier onlySupportedToken(address _token) { @@ -81,8 +95,8 @@ contract NativeStaking is { IERC20(_token).safeTransferFrom(_account, address(this), _amount); - userStakeInfo[_account][_operator][_token] += _amount; - operatorStakeInfo[_operator][_token].delegatedStake += _amount; + stakedAmounts[_account][_operator][_token] += _amount; + operatorStakedAmounts[_operator][_token] += _amount; // NativeStakingReward contract will read staking amount info from this contract // and update reward related states @@ -101,14 +115,14 @@ contract NativeStaking is { IERC20(_token).safeTransferFrom(_operator, address(this), _amount); - operatorStakeInfo[_operator][_token].selfStake += _amount; + operatorStakedAmounts[_operator][_token] += _amount; emit SelfStaked(_operator, _token, _amount, block.timestamp); } // This should update StakingManger's state function withdrawStake(address _account, address _operator, address _token, uint256 _amount) external nonReentrant { - require(userStakeInfo[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); + require(stakedAmounts[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); // TODO: check locked time @@ -116,8 +130,8 @@ contract NativeStaking is IERC20(_token).safeTransfer(msg.sender, _amount); - userStakeInfo[msg.sender][_operator][_token] -= _amount; - operatorStakeInfo[_operator][_token].delegatedStake -= _amount; + stakedAmounts[msg.sender][_operator][_token] -= _amount; + operatorStakedAmounts[_operator][_token] -= _amount; INativeStakingReward(nativeStakingReward).update(_account, _token, _operator); @@ -125,102 +139,27 @@ contract NativeStaking is } function withdrawSelfStake(address operator, address token, uint256 amount) external nonReentrant { - require(operatorStakeInfo[operator][token].selfStake >= amount, "Insufficient selfstake"); + require(operatorStakedAmounts[operator][token] >= amount, "Insufficient selfstake"); IERC20(token).safeTransfer(operator, amount); - operatorStakeInfo[operator][token].selfStake -= amount; + operatorStakedAmounts[operator][token] -= amount; emit SelfStakeWithdrawn(operator, token, amount, block.timestamp); } /*======================================== Getters ========================================*/ - function getPoolStake(address _operator, address _token) external view returns (uint256) { - return operatorStakeInfo[_operator][_token].delegatedStake; - } - - function getStakeAmountList(address _operator) external view returns (address[] memory _operators, uint256[] memory _amounts) {} - - function isSupportedToken(address _token) external view returns (bool) {} - - function getDelegatedStake(address _operator, address _token) - external - view - onlySupportedToken(_token) - returns (uint256) - { - return operatorStakeInfo[_operator][_token].delegatedStake; - } - - // TODO: deduct locked amount - function getDelegateStakeActive(address _operator, address _token) - external - view - onlySupportedToken(_token) - returns (uint256) - { - return operatorStakeInfo[_operator][_token].delegatedStake; - } - - // Returns the list of tokenSet staked by the operator and the amounts - // function getDelegatedStakes(address _operator) - // external - // view - // returns (address[] memory _tokens, uint256[] memory _amounts) - // { - // uint256 len = tokenSet.length(); - - // for (uint256 i = 0; i < len; i++) { - // _tokens[i] = tokenSet.at(i); - // _amounts[i] = operatorStakeInfo[_operator][_tokens[i]].delegatedStake; - // } - // } - - function getSelfStake(address _operator, address _token) - external - view - onlySupportedToken(_token) - returns (uint256) - { - return operatorStakeInfo[_operator][_token].selfStake; - } - - - function getOperatorTotalStake(address _operator, address _token) - external - view - onlySupportedToken(_token) - returns (uint256) - { - OperatorStake memory _stakeInfo = operatorStakeInfo[_operator][_token]; - return _stakeInfo.delegatedStake + _stakeInfo.selfStake; + function getStakeAmount(address _token) external view returns (uint256) { + return totalStakedAmounts[_token]; } - function getOperatorTotalStakes(address _operator) - external - view - returns (address[] memory _tokens, uint256[] memory _amounts) - { - uint256 len = tokenSet.length(); - - for (uint256 i = 0; i < len; i++) { - OperatorStake memory _stakeInfo = operatorStakeInfo[_operator][tokenSet.at(i)]; - _tokens[i] = tokenSet.at(i); - _amounts[i] = _stakeInfo.delegatedStake + _stakeInfo.selfStake; - } + function getActiveStakeAmount(address _token) external view returns (uint256) { + return totalStakedAmounts[_token] - totalLockedAmounts[_token]; } - function getTokenTotalStake(address _token) external view onlySupportedToken(_token) returns (uint256) { - uint256 len = operatorSet.length(); - uint256 totalStake; - - for (uint256 i = 0; i < len; i++) { - totalStake += operatorStakeInfo[operatorSet.at(i)][_token].delegatedStake - + operatorStakeInfo[operatorSet.at(i)][_token].selfStake; - } - - return totalStake; + function isSupportedToken(address _token) external view returns (bool) { + return tokenSet.contains(_token); } function isSupportedSignature(bytes4 sig) external view returns (bool) { @@ -242,17 +181,35 @@ contract NativeStaking is } /*======================================== StakingManager ========================================*/ - function lockStake(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 _selfStakeLock) external { + function lockStake(uint256 _jobId, address _operator) external { // TODO: only staking manager - - lockInfo[_jobId] = NativeStakingLock(_token, _delegatedStakeLock, _selfStakeLock); + address _token = _selectLockToken(); + uint256 _amountToLock = amountToLock[_token]; + require(operatorStakedAmounts[_operator][_token] >= _amountToLock, "Insufficient stake to lock"); + + // lock stake + jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); + operatorLockedAmounts[_operator][_token] += _amountToLock; + totalLockedAmounts[_token] += _amountToLock; + // TODO: emit event } + function _selectLockToken() internal returns(address) { + if (tokenSet.length() == 1) { + return tokenSet.at(0); + } else { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); + uint256 tokenIdx = randomNumber % tokenSet.length(); + return tokenSet.at(tokenIdx); + } + } + function unlockStake(uint256 _jobId) external { // TODO: only staking manager - lockInfo[_jobId] = NativeStakingLock(address(0), 0, 0); + // TODO: need to mark something that indicates that job is completed? + jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); // TODO: emit event } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 66503a6..c8a7620 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -27,21 +27,20 @@ contract StakingManager is // TODO: Staking Pool Set EnumerableSet.AddressSet private stakingPoolSet; - // TODO: Staking Pool flag - // mapping(address pool => bool isEnabled) private stakingPoolStatus; - // mapping(address pool => uint256 weight) private stakingPoolWeight; + mapping(address pool => bool isEnabled) private stakingPoolStatus; + mapping(address pool => uint256 weight) private stakingPoolWeight; mapping(address pool => PoolConfig config) private poolConfig; // TODO: getter for retreiving the each pool's lock amount mapping(uint256 jobId => uint256 lockAmount) private lockInfo; // total lock amount and unlock timestamp - + uint256 unlockEpoch; uint256 stakeDataTransmitterShare; struct PoolConfig { uint256 weight; - uint256 minStake; + uint256 minStake; // min stake for bool enabled; } @@ -64,7 +63,7 @@ contract StakingManager is // locked stake will be unlocked after an epoch if no slas result is submitted // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) - function onJobCreation(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 _selfStakeLock) external { + function onJobCreation(uint256 _jobId, address _operator) external { // TODO: only jobManager // TODO: lock selfstake, Native Staking delegated stake, Symbiotic Stake @@ -74,30 +73,10 @@ contract StakingManager is for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - //! TODO: check github comments if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled - if(!IKalypsoStaking(pool).isSupportedToken(_token)) continue; // skip if the token is not supported by the pool - - uint256 poolStake = IKalypsoStaking(pool).getPoolStake(pool, _token); - uint256 minStake = poolConfig[pool].minStake; - - // skip if the pool stake is less than the minStake - //? case when lockAmount > minStake? - uint256 lockAmount = 0; - if(poolStake >= minStake) { - uint256 poolLockAmount = _calcLockAmount(poolStake, pool); // TODO: lock amount will be fixed - lockAmount += poolLockAmount; - IKalypsoStaking(pool).lockStake(_jobId, _token, _selfStakeLock, _delegatedStakeLock); - } - } - } - //! TODO: remove this and read from mapping - // TODO: fix this - function _calcLockAmount(uint256 amount, address pool) internal view returns (uint256) { - uint256 weight = poolConfig[pool].weight; - - return (amount * weight) / 10000; + IKalypsoStaking(pool).lockStake(_jobId, _operator); + } } // TODO @@ -159,10 +138,12 @@ contract StakingManager is // TODO: only admin } + // TODO: check if needed function setUnlockEpoch(uint256 _unlockEpoch) external { // TODO: check if the unlockEpoch is longer than the proofDeadline } + // when job is closed, the reward will be distributed based on the share function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external { // TODO: only admin diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index c00ab8f..2beef14 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -22,6 +22,10 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); + /*======================================== Config ========================================*/ + mapping(address pool => mapping(address token => uint256 amount)) public stakeLockAmounts; + mapping(address operator => uint256 amount) public selfStakeLockAmounts; + // TODO: redundant to L1 Data EnumerableSet.AddressSet tokenSet; mapping(address vault => address token) public vaultToToken; @@ -140,12 +144,12 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked - function lockStake(uint256 _jobId, address _token, uint256 _delegatedStakeLock, uint256 /* selfStakeLock */) external { - require(isSupportedToken(_token), "Token not supported"); + function lockStake(uint256 _jobId, address /* operator */) external { + // require(isSupportedToken(_token), "Token not supported"); - // Store transmitter address to reward when job is closed - address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); + // // Store transmitter address to reward when job is closed + // address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; + // lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); // TODO: emit event } From 9f547982bed7c803a39c1b9d8e941db011be0f28 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 24 Sep 2024 14:01:40 +0900 Subject: [PATCH 060/158] refactor _selectToken logic --- .../staking/l2_contracts/NativeStaking.sol | 11 +++++----- .../staking/l2_contracts/SymbioticStaking.sol | 21 +++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 5384593..908313b 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -196,13 +196,14 @@ contract NativeStaking is } function _selectLockToken() internal returns(address) { - if (tokenSet.length() == 1) { - return tokenSet.at(0); - } else { + require(tokenSet.length() > 0, "No supported token"); + + uint256 idx; + if (tokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - uint256 tokenIdx = randomNumber % tokenSet.length(); - return tokenSet.at(tokenIdx); + uint256 idx = randomNumber % tokenSet.length(); } + return tokenSet.at(idx); } function unlockStake(uint256 _jobId) external { diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 2beef14..ef20c63 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -145,15 +145,28 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked function lockStake(uint256 _jobId, address /* operator */) external { - // require(isSupportedToken(_token), "Token not supported"); + uint256 _token = _selectLockToken(); + require() - // // Store transmitter address to reward when job is closed - // address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - // lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); + + // Store transmitter address to reward when job is closed + address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; + lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); // TODO: emit event } + function _selectLockToken() internal returns(address) { + require(tokenSet.length() > 0, "No supported token"); + + uint256 idx; + if (tokenSet.length() > 1) { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); + uint256 idx = randomNumber % tokenSet.length(); + } + return tokenSet.at(idx); + } + // TODO: check if delegatedStake also gets unlocked function unlockStake(uint256 _jobId) external { // TODO: only staking manager From 2b5a282e0396a9bdcb5d20928b8ad609fc78e462 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 24 Sep 2024 17:10:45 +0900 Subject: [PATCH 061/158] move lockStake logic to SymbioticStaking from StakingManager --- .../interfaces/staking/ISymbioticStaking.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 4 +- .../staking/l2_contracts/SymbioticStaking.sol | 38 ++++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 0e95e0a..a383d9f 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -31,7 +31,7 @@ interface ISymbioticStaking is IKalypsoStaking { struct SlashResult { uint256 slashAmount; - address rewardAddress; + address rewardAddress; // address that transmitted slash reqeust to L1 Vault } struct ConfirmedTimestamp { diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 908313b..c32fc32 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -195,13 +195,13 @@ contract NativeStaking is // TODO: emit event } - function _selectLockToken() internal returns(address) { + function _selectLockToken() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); uint256 idx; if (tokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - uint256 idx = randomNumber % tokenSet.length(); + idx = randomNumber % tokenSet.length(); } return tokenSet.at(idx); } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index ef20c63..44cede9 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -23,8 +23,8 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); /*======================================== Config ========================================*/ - mapping(address pool => mapping(address token => uint256 amount)) public stakeLockAmounts; - mapping(address operator => uint256 amount) public selfStakeLockAmounts; + mapping(address token => uint256 amount) public minStakeAmount; + mapping(address token => uint256 amount) public amountToLock; // TODO: redundant to L1 Data EnumerableSet.AddressSet tokenSet; @@ -35,10 +35,12 @@ contract SymbioticStaking is ISymbioticStaking { mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received - // TODO: discuss this later (problem of who submitted the partial txs) - mapping(address operator => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) operatorStakes; - mapping(address vault => mapping(address token => mapping(uint256 captureTimestamp => uint256 stake))) vaultStakes; - mapping(uint256 jobId => mapping(uint256 captureTimestamp => SlashResult slashResult)) slashResults; // TODO: need to check actual slashing timestamp? + // staked amount for each operator + mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; + // staked amount for each vault + mapping(uint256 captureTimestamp => mapping(address vault => mapping(address token => uint256 stakeAmount))) vaultStakedAmounts; + // slash result for each job + mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashResult)) slashResults; ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received @@ -144,25 +146,25 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked - function lockStake(uint256 _jobId, address /* operator */) external { - uint256 _token = _selectLockToken(); - require() - + function lockStake(uint256 _jobId, address _operator) external { + address _token = _selectLockToken(); + uint256 stakedAmount = getOperatorStake(_operator, _token); + require(stakedAmount >= minStakeAmount[_token], "Insufficient stake amount"); // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - lockInfo[_jobId] = SymbioticStakingLock(_token, _delegatedStakeLock, transmitter); + lockInfo[_jobId] = SymbioticStakingLock(_token, amountToLock[_token], transmitter); // TODO: emit event } - function _selectLockToken() internal returns(address) { + function _selectLockToken() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); - + uint256 idx; if (tokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - uint256 idx = randomNumber % tokenSet.length(); + idx = randomNumber % tokenSet.length(); } return tokenSet.at(idx); } @@ -175,8 +177,8 @@ contract SymbioticStaking is ISymbioticStaking { // TODO: emit event } - function getPoolStake(address _operator, address _token) external view returns (uint256) { - return operatorStakes[_operator][_token][lastConfirmedTimestamp()]; + function getOperatorStake(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; } /*======================================== Helpers ========================================*/ @@ -226,7 +228,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _operatorSnapshots.length; i++) { OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; - operatorStakes[_operatorSnapshot.operator][_operatorSnapshot.token][_captureTimestamp] = + operatorStakedAmounts[_captureTimestamp][_operatorSnapshot.operator][_operatorSnapshot.token] = _operatorSnapshot.stake; // TODO: emit event for each update? @@ -237,7 +239,7 @@ contract SymbioticStaking is ISymbioticStaking { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; - vaultStakes[_vaultSnapshot.vault][_vaultSnapshot.token][_captureTimestamp] = _vaultSnapshot.stake; + vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.token] = _vaultSnapshot.stake; // TODO: emit event for each update? } From a766094a8de027ac2037d12d0a640ee03f55dcdd Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 24 Sep 2024 17:34:19 +0900 Subject: [PATCH 062/158] refactor code --- .../interfaces/staking/IStakingManager.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 21 ++++++------ .../staking/l2_contracts/NativeStaking.sol | 32 +++++++++++-------- .../staking/l2_contracts/StakingManager.sol | 30 ++++++++--------- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index fa722c4..9f0aa53 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; interface IStakingManager { - function onJobCreation(uint256 jobId, address operator, address token, uint256 amountToLock) external; + function onJobCreation(uint256 jobId, address operator) external; function onJobCompletion(uint256 jobId, address token) external; diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 877218f..cd96f25 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -8,7 +8,6 @@ import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. */ contract JobManager { - uint256 constant JOB_DURATION = 1 days; IStakingManager stakingManager; @@ -23,15 +22,21 @@ contract JobManager { mapping(uint256 => JobInfo) public jobs; - // TODO: token => lockAmount logic - function createJob(uint256 jobId, address operator, address token, uint256 amountToLock) external { + function createJob(uint256 jobId, address operator) external { // TODO: called only from Kalypso Protocol // TODO: create a job and record StakeData Transmitter who submitted capture timestamp // TODO: call creation function in StakingManager - stakingManager.onJobCreation(jobId, operator, token, amountToLock); // lock stake + stakingManager.onJobCreation(jobId, operator); // lock stake + } + + /** + * @notice Submit Single Proof + */ + function submitProof(uint256 jobId, bytes calldata proof) public { + _verifyProof(jobId, proof); } /** @@ -43,7 +48,6 @@ contract JobManager { // TODO: close job and distribute rewards uint256 len = jobIds.length; - for (uint256 idx = 0; idx < len; idx++) { _verifyProof(jobIds[idx], proofs[idx]); @@ -53,13 +57,6 @@ contract JobManager { } - /** - * @notice Submit Single Proof - */ - function submitProof(uint256 jobId, bytes calldata proof) public { - _verifyProof(jobId, proof); - } - function _verifyProof(uint256 jobId, bytes calldata proof) internal { // TODO } diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index c32fc32..72168a3 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -30,13 +30,13 @@ contract NativeStaking is address public nativeStakingReward; /*======================================== Config ========================================*/ - // config + + /* Config */ mapping(address token => uint256 minStakeamount) public minStakeAmount; mapping(address token => uint256 lockAmount) public amountToLock; mapping(bytes4 sig => bool isSupported) private supportedSignatures; - // stake - + /* Stake */ // total staked amounts for each operator, includes selfStake and delegatedStake amount mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; // selfstake if account == operator @@ -44,7 +44,7 @@ contract NativeStaking is // total staked amount for each token mapping(address token => uint256 amount) public totalStakedAmounts; - // lock + /* Locked Stakes */ // TODO: check if mappings below are needed mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; // includes selfStake and delegatedStake amount @@ -65,17 +65,7 @@ contract NativeStaking is _; } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC165Upgradeable, AccessControlUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId); - } - function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} function initialize(address _admin) public initializer { __Context_init_unchained(); @@ -214,4 +204,18 @@ contract NativeStaking is // TODO: emit event } + + /*======================================== Overrides ========================================*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index c8a7620..149ffa0 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -24,20 +24,16 @@ contract StakingManager is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - // TODO: Staking Pool Set + address public jobManager; + EnumerableSet.AddressSet private stakingPoolSet; mapping(address pool => bool isEnabled) private stakingPoolStatus; mapping(address pool => uint256 weight) private stakingPoolWeight; - mapping(address pool => PoolConfig config) private poolConfig; - - // TODO: getter for retreiving the each pool's lock amount - mapping(uint256 jobId => uint256 lockAmount) private lockInfo; // total lock amount and unlock timestamp uint256 unlockEpoch; uint256 stakeDataTransmitterShare; - struct PoolConfig { uint256 weight; uint256 minStake; // min stake for @@ -50,6 +46,11 @@ contract StakingManager is uint256 unlockTimestamp; } + modifier onlyJobManager() { + require(msg.sender == jobManager, "StakingManager: Only JobManager"); + _; + } + function initialize(address _admin) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); @@ -57,22 +58,19 @@ contract StakingManager is __UUPSUpgradeable_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + // TODO: set jobmanager } // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) // locked stake will be unlocked after an epoch if no slas result is submitted // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) - function onJobCreation(uint256 _jobId, address _operator) external { - // TODO: only jobManager - - // TODO: lock selfstake, Native Staking delegated stake, Symbiotic Stake - + function onJobCreation(uint256 _jobId, address _operator) external onlyJobManager { uint256 len = stakingPoolSet.length(); - + for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled IKalypsoStaking(pool).lockStake(_jobId, _operator); @@ -85,9 +83,7 @@ contract StakingManager is // } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId) external { - // TODO: only jobManager - + function onJobCompletion(uint256 _jobId) external onlyJobManager { // TODO: unlock the locked stakes uint256 len = stakingPoolSet.length(); for(uint256 i = 0; i < len; i++) { @@ -157,7 +153,7 @@ contract StakingManager is // as the weight is in percentage, the sum of the shares should be 10000 // TODO: sum of enabled pools should be 10000 - require(sum == 10000, "Invalid Shares"); + require(sum == 1e18, "Invalid Shares"); for(uint256 i = 0; i < _pools.length; i++) { poolConfig[_pools[i]].weight = _shares[i]; From 8a1e5c262fed599b4727033313f5098b62e98fec Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 10:48:09 +0900 Subject: [PATCH 063/158] fix SD, TC to be more verbose name --- .../staking/l2_contracts/SymbioticStaking.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 44cede9..0a187d0 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -10,8 +10,8 @@ contract SymbioticStaking is ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; //! TODO: make variable more descriptive - uint256 SD; - uint256 TC; + uint256 submissionCooldown; + uint256 transmitterComission; bytes32 public constant OPERATOR_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; @@ -183,7 +183,7 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Helpers ========================================*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { - require(block.timestamp >= lastConfirmedTimestamp() + SD, "Cooldown period not passed"); + require(block.timestamp >= lastConfirmedTimestamp() + submissionCooldown, "Cooldown period not passed"); require(_numOfTxs > 0, "Invalid length"); require(_index < _numOfTxs, "Invalid index"); @@ -234,7 +234,6 @@ contract SymbioticStaking is ISymbioticStaking { // TODO: emit event for each update? } } - function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; @@ -258,13 +257,13 @@ contract SymbioticStaking is ISymbioticStaking { } function _completeSubmission(uint256 _captureTimestamp) internal { - // TODO: calc `TC` based on last submission + // TODO: calc `transmitterComission` based on last submission ConfirmedTimestamp memory confirmedTimestamp = ConfirmedTimestamp(_captureTimestamp, block.timestamp, msg.sender); confirmedTimestamps.push(confirmedTimestamp); - // TODO: calculate rewards for the transmitter based on TC - // TODO: Data transmitter should get TC% of the rewards - // TODO: "TC" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + SD)" + // TODO: calculate rewards for the transmitter based on transmitterComission + // TODO: Data transmitter should get transmitterComission% of the rewards + // TODO: "transmitterComission" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + submissionCooldown)" // TODO: emit SubmissionCompleted } From f00b55a0bc8c0ba69759f45f91b68e6791642dad Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 10:48:32 +0900 Subject: [PATCH 064/158] minor fix --- contracts/staking/l2_contracts/JobManager.sol | 5 +++-- contracts/staking/l2_contracts/NativeStaking.sol | 16 ++++++++++------ .../staking/l2_contracts/NativeStakingReward.sol | 2 -- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index cd96f25..4405bd6 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -22,6 +22,7 @@ contract JobManager { mapping(uint256 => JobInfo) public jobs; + // TODO: check paramter for job details function createJob(uint256 jobId, address operator) external { // TODO: called only from Kalypso Protocol @@ -45,8 +46,8 @@ contract JobManager { function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external { require(jobIds.length == proofs.length, "Invalid Length"); - // TODO: close job and distribute rewards + uint256 len = jobIds.length; for (uint256 idx = 0; idx < len; idx++) { _verifyProof(jobIds[idx], proofs[idx]); @@ -58,7 +59,7 @@ contract JobManager { } function _verifyProof(uint256 jobId, bytes calldata proof) internal { - // TODO + // TODO: verify proof } function setStakingManager(address _stakingManager) external { diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 72168a3..65e8d04 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -28,6 +28,7 @@ contract NativeStaking is EnumerableSet.AddressSet private operatorSet; // TODO: check if needed address public nativeStakingReward; + address public stakingManager; /*======================================== Config ========================================*/ @@ -65,7 +66,10 @@ contract NativeStaking is _; } - + modifier onlyStakingManager() { + require(msg.sender == stakingManager, "Only StakingManager"); + _; + } function initialize(address _admin) public initializer { __Context_init_unchained(); @@ -171,8 +175,7 @@ contract NativeStaking is } /*======================================== StakingManager ========================================*/ - function lockStake(uint256 _jobId, address _operator) external { - // TODO: only staking manager + function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); uint256 _amountToLock = amountToLock[_token]; require(operatorStakedAmounts[_operator][_token] >= _amountToLock, "Insufficient stake to lock"); @@ -196,12 +199,13 @@ contract NativeStaking is return tokenSet.at(idx); } - function unlockStake(uint256 _jobId) external { - // TODO: only staking manager - + function unlockStake(uint256 _jobId) external onlyStakingManager { // TODO: need to mark something that indicates that job is completed? jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); + // TODO: distribute reward + + // TODO: emit event } diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index fc7d7b4..41376e3 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -66,8 +66,6 @@ contract NativeStakingReward is //-------------------------------- NativeStaking start --------------------------------// - - function claimReward(address token) public { } From cd625995d1ebfe16d6de129fc402ec2df6f25699 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 10:49:35 +0900 Subject: [PATCH 065/158] add comments --- contracts/staking/l2_contracts/SymbioticStaking.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 0a187d0..e6da4e6 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -9,9 +9,8 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet contract SymbioticStaking is ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; - //! TODO: make variable more descriptive - uint256 submissionCooldown; - uint256 transmitterComission; + uint256 submissionCooldown; // 18 decimal (in seconds) + uint256 transmitterComission; // 18 decimal (in percentage) bytes32 public constant OPERATOR_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; From 2c6cb90c23cc90ec90f5fa918fc6d52ff6be683c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 10:51:19 +0900 Subject: [PATCH 066/158] comment out unnecessary variables --- contracts/staking/l2_contracts/SymbioticStaking.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index e6da4e6..5eb3d51 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -27,8 +27,8 @@ contract SymbioticStaking is ISymbioticStaking { // TODO: redundant to L1 Data EnumerableSet.AddressSet tokenSet; - mapping(address vault => address token) public vaultToToken; - mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token + // mapping(address vault => address token) public vaultToToken; + // mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token /* Symbiotic Data Transmission */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received @@ -270,9 +270,9 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Getters ========================================*/ // TODO: remove - function getVaultToken(address _vault) public view returns (address) { - return vaultToToken[_vault]; - } + // function getVaultToken(address _vault) public view returns (address) { + // return vaultToToken[_vault]; + // } function lastConfirmedTimestamp() public view returns (uint256) { return confirmedTimestamps[confirmedTimestamps.length - 1].capturedTimestamp; From a2c92ede808a57eb22ff1cc96a1aab01998604c2 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 11:50:56 +0900 Subject: [PATCH 067/158] force Snapshot submission to be sequential --- .../interfaces/staking/ISymbioticStaking.sol | 4 +-- .../staking/l2_contracts/SymbioticStaking.sol | 27 +++++++++---------- .../l2_contracts/SymbioticStakingReward.sol | 3 +-- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index a383d9f..fe38959 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -7,8 +7,8 @@ interface ISymbioticStaking is IKalypsoStaking { // function stakeOf(address _operator, address _token) external view returns (uint256); struct SnapshotTxCountInfo { - uint256 count; - uint256 numOfTxs; + uint256 idxToSubmit; // idx of pratial snapshot tx to submit + uint256 numOfTxs; // total number of txs for the snapshot } struct OperatorSnapshot { diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 5eb3d51..1f30abe 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -56,7 +56,7 @@ contract SymbioticStaking is ISymbioticStaking { // Transmitter submits staking data snapshot // This should update StakingManger's state - // TODO + // TODO: consolidate with submitVaultSnapshot function submitOperatorSnapshot( uint256 _index, uint256 _numOfTxs, // number of total transactions @@ -69,6 +69,7 @@ contract SymbioticStaking is ISymbioticStaking { _verifySignature(_index, _numOfTxs, _captureTimestamp, _operatorSnapshotData, _signature); // main update logic + // TODO: consolidate this into VaultSnapshot[] OperatorSnapshot[] memory _operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); _updateOperatorSnapshotInfo(_captureTimestamp, _operatorSnapshots); @@ -77,7 +78,7 @@ contract SymbioticStaking is ISymbioticStaking { _updateTxCountInfo(_numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); // when all chunks of OperatorSnapshot are submitted - if (_snapshot.count == _snapshot.numOfTxs) { + if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; } @@ -105,7 +106,7 @@ contract SymbioticStaking is ISymbioticStaking { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted - if (_snapshot.count == _snapshot.numOfTxs) { + if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; } @@ -132,7 +133,7 @@ contract SymbioticStaking is ISymbioticStaking { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted - if (_snapshot.count == _snapshot.numOfTxs) { + if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; } @@ -182,13 +183,15 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Helpers ========================================*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { - require(block.timestamp >= lastConfirmedTimestamp() + submissionCooldown, "Cooldown period not passed"); - require(_numOfTxs > 0, "Invalid length"); - require(_index < _numOfTxs, "Invalid index"); + // snapshot cannot be submitted before the cooldown period from the last confirmed timestamp (completed snapshot submission) + require(block.timestamp >= (lastConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); + + SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; - require(snapshot.count < snapshot.numOfTxs, "Snapshot fully submitted already"); + require(_index == snapshot.idxToSubmit, "Invalid index"); + require(snapshot.idxToSubmit < snapshot.numOfTxs, "Snapshot fully submitted already"); require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); bytes32 mask; @@ -203,13 +206,12 @@ contract SymbioticStaking is ISymbioticStaking { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; // increase count by 1 - txCountInfo[_captureTimestamp][msg.sender][_type].count += 1; + txCountInfo[_captureTimestamp][msg.sender][_type].idxToSubmit += 1; // update length if 0 if (_snapshot.numOfTxs == 0) { txCountInfo[_captureTimestamp][msg.sender][_type].numOfTxs = _numOfTxs; } - } function _verifySignature(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes memory _data, bytes memory _signature) internal { @@ -269,11 +271,6 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Getters ========================================*/ - // TODO: remove - // function getVaultToken(address _vault) public view returns (address) { - // return vaultToToken[_vault]; - // } - function lastConfirmedTimestamp() public view returns (uint256) { return confirmedTimestamps[confirmedTimestamps.length - 1].capturedTimestamp; } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 9002acf..57a1f9c 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -130,8 +130,7 @@ contract SymbioticStakingReward is claimableRewards[_vault] = 0; - // TODO - IERC20(_getRewardToken(_vault)).safeTransfer(_vault, rewardAmount); + // TODO: let the user claim reward of all rewardTokens // TODO: emit event } From 8d200069a1a9740c416238dca2e3d509feea4d6d Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 15:53:37 +0900 Subject: [PATCH 068/158] consolidate OperatorSnapshot into VaultSnapshot --- .../interfaces/staking/ISymbioticStaking.sol | 8 +- .../staking/l2_contracts/NativeStaking.sol | 2 + .../staking/l2_contracts/SymbioticStaking.sol | 93 +++++++------------ 3 files changed, 36 insertions(+), 67 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index fe38959..3e38153 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -11,14 +11,8 @@ interface ISymbioticStaking is IKalypsoStaking { uint256 numOfTxs; // total number of txs for the snapshot } - struct OperatorSnapshot { - address operator; - address token; - uint256 stake; - } - - //! TODO: add operator struct VaultSnapshot { + address operator; address vault; address token; uint256 stake; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 65e8d04..f46ed3e 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -200,6 +200,8 @@ contract NativeStaking is } function unlockStake(uint256 _jobId) external onlyStakingManager { + // TODO: consider the case when new pool is added during job + // TODO: need to mark something that indicates that job is completed? jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 1f30abe..16ca37c 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -12,25 +12,21 @@ contract SymbioticStaking is ISymbioticStaking { uint256 submissionCooldown; // 18 decimal (in seconds) uint256 transmitterComission; // 18 decimal (in percentage) - bytes32 public constant OPERATOR_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; - bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; - bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000100; - bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000111; + /* Job Status */ + bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; + bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000011; - bytes32 public constant OPERATOR_SNAPSHOT = keccak256("OPERATOR_SNAPSHOT"); bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); - /*======================================== Config ========================================*/ + /* Config */ mapping(address token => uint256 amount) public minStakeAmount; mapping(address token => uint256 amount) public amountToLock; - // TODO: redundant to L1 Data EnumerableSet.AddressSet tokenSet; - // mapping(address vault => address token) public vaultToToken; - // mapping(address token => uint256 numVaults) public tokenToNumVaults; // number of vaults that support the token - /* Symbiotic Data Transmission */ + /* Symbiotic Snapshot */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received @@ -52,41 +48,12 @@ contract SymbioticStaking is ISymbioticStaking { /* Staking */ mapping(uint256 jobId => SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake + mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; + /*======================================== L1 to L2 Transmission ========================================*/ // Transmitter submits staking data snapshot // This should update StakingManger's state - // TODO: consolidate with submitVaultSnapshot - function submitOperatorSnapshot( - uint256 _index, - uint256 _numOfTxs, // number of total transactions - uint256 _captureTimestamp, - bytes memory _operatorSnapshotData, - bytes memory _signature - ) external { - _checkValidity(_index, _numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); - - _verifySignature(_index, _numOfTxs, _captureTimestamp, _operatorSnapshotData, _signature); - - // main update logic - // TODO: consolidate this into VaultSnapshot[] - OperatorSnapshot[] memory _operatorSnapshots = abi.decode(_operatorSnapshotData, (OperatorSnapshot[])); - _updateOperatorSnapshotInfo(_captureTimestamp, _operatorSnapshots); - - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; - - _updateTxCountInfo(_numOfTxs, _captureTimestamp, OPERATOR_SNAPSHOT); - - // when all chunks of OperatorSnapshot are submitted - if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; - } - - if (_isCompleteStatus(_captureTimestamp)) { - _completeSubmission(_captureTimestamp); - } - } - function submitVaultSnapshot( uint256 _index, uint256 _numOfTxs, // number of total transactions @@ -94,20 +61,23 @@ contract SymbioticStaking is ISymbioticStaking { bytes memory _vaultSnapshotData, bytes memory _signature ) external { + + _checkTransmitterRegistration(_captureTimestamp); + _checkValidity(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); // main update logic VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (VaultSnapshot[])); - _updateVaultSnapshotInfo(_captureTimestamp, _vaultSnapshots); + _updateSnapshotInfo(_captureTimestamp, _vaultSnapshots); _updateTxCountInfo(_numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; + submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT_MASK; } if (_isCompleteStatus(_captureTimestamp)) { @@ -131,10 +101,10 @@ contract SymbioticStaking is ISymbioticStaking { _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][OPERATOR_SNAPSHOT]; + SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= OPERATOR_SNAPSHOT_MASK; + submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT; } if (_isCompleteStatus(_captureTimestamp)) { @@ -171,6 +141,8 @@ contract SymbioticStaking is ISymbioticStaking { // TODO: check if delegatedStake also gets unlocked function unlockStake(uint256 _jobId) external { + // TODO: consider the case when new pool is added during job + // TODO: only staking manager lockInfo[_jobId].amount = 0; @@ -195,13 +167,21 @@ contract SymbioticStaking is ISymbioticStaking { require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); bytes32 mask; - if (_type == OPERATOR_SNAPSHOT) mask = OPERATOR_SNAPSHOT_MASK; - else if (_type == VAULT_SNAPSHOT) mask = VAULT_SNAPSHOT_MASK; + if (_type == VAULT_SNAPSHOT) mask = VAULT_SNAPSHOT_MASK; else if (_type == SLASH_RESULT) mask = SLASH_RESULT_MASK; require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Snapshot fully submitted already"); } + function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { + if(registeredTransmitters[_captureTimestamp] == address(0)) { + // once transmitter is registered, another transmitter cannot submit the snapshot for the same capturetimestamp + registeredTransmitters[_captureTimestamp] = msg.sender; + } else { + require(registeredTransmitters[_captureTimestamp] == msg.sender, "Not registered transmitter"); + } + } + function _updateTxCountInfo(uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; @@ -223,24 +203,17 @@ contract SymbioticStaking is ISymbioticStaking { return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; } - function _updateOperatorSnapshotInfo(uint256 _captureTimestamp, OperatorSnapshot[] memory _operatorSnapshots) - internal - { - for (uint256 i = 0; i < _operatorSnapshots.length; i++) { - OperatorSnapshot memory _operatorSnapshot = _operatorSnapshots[i]; - - operatorStakedAmounts[_captureTimestamp][_operatorSnapshot.operator][_operatorSnapshot.token] = - _operatorSnapshot.stake; - // TODO: emit event for each update? - } - } - function _updateVaultSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { + function _updateSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; + // update vault staked amount vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.token] = _vaultSnapshot.stake; + // update operator staked amount + operatorStakedAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.token] += _vaultSnapshot.stake; + // TODO: emit event for each update? } } From 6d0c15f96f4545009f284c3b17dd81987ff04445 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 17:03:26 +0900 Subject: [PATCH 069/158] remove unnecessary comment --- contracts/staking/l2_contracts/SymbioticStaking.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 16ca37c..43f2e3f 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -51,8 +51,6 @@ contract SymbioticStaking is ISymbioticStaking { mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; /*======================================== L1 to L2 Transmission ========================================*/ - // Transmitter submits staking data snapshot - // This should update StakingManger's state function submitVaultSnapshot( uint256 _index, From 2c8a0bcbe9face17d02fa783120994bc6ef07876 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 17:03:57 +0900 Subject: [PATCH 070/158] fix typo --- contracts/staking/l2_contracts/SymbioticStaking.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 43f2e3f..72d869b 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -59,7 +59,6 @@ contract SymbioticStaking is ISymbioticStaking { bytes memory _vaultSnapshotData, bytes memory _signature ) external { - _checkTransmitterRegistration(_captureTimestamp); _checkValidity(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); @@ -173,7 +172,7 @@ contract SymbioticStaking is ISymbioticStaking { function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { if(registeredTransmitters[_captureTimestamp] == address(0)) { - // once transmitter is registered, another transmitter cannot submit the snapshot for the same capturetimestamp + // once transmitter is registered, other transmitters cannot submit the snapshot for the same capturetimestamp registeredTransmitters[_captureTimestamp] = msg.sender; } else { require(registeredTransmitters[_captureTimestamp] == msg.sender, "Not registered transmitter"); From c67c32b3773d17c7448851646a784c96f1be16ef Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 23:56:35 +0900 Subject: [PATCH 071/158] fix transmitterComission to baseTransmitterComission --- contracts/staking/l2_contracts/SymbioticStaking.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 72d869b..92d0fb3 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -10,7 +10,7 @@ contract SymbioticStaking is ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; uint256 submissionCooldown; // 18 decimal (in seconds) - uint256 transmitterComission; // 18 decimal (in percentage) + uint256 baseTransmitterComission; // 18 decimal (in percentage) /* Job Status */ bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; From b202fe83c7cf8cf28e83a02fe51de39834106ef8 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 23:58:30 +0900 Subject: [PATCH 072/158] fix IKalysoStaking to IStakingPool --- contracts/interfaces/staking/INativeStaking.sol | 4 ++-- .../staking/{IKalypsoStaking.sol => IStakingPool.sol} | 2 +- contracts/interfaces/staking/ISymbioticStaking.sol | 4 ++-- contracts/staking/l2_contracts/StakingManager.sol | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) rename contracts/interfaces/staking/{IKalypsoStaking.sol => IStakingPool.sol} (95%) diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 4cb8b89..b19e7ec 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {IKalypsoStaking} from "../staking/IKalypsoStaking.sol"; +import {IStakingPool} from "../staking/IStakingPool.sol"; -interface INativeStaking is IKalypsoStaking { +interface INativeStaking is IStakingPool { // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); diff --git a/contracts/interfaces/staking/IKalypsoStaking.sol b/contracts/interfaces/staking/IStakingPool.sol similarity index 95% rename from contracts/interfaces/staking/IKalypsoStaking.sol rename to contracts/interfaces/staking/IStakingPool.sol index bbca976..5e3628b 100644 --- a/contracts/interfaces/staking/IKalypsoStaking.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -interface IKalypsoStaking { +interface IStakingPool { function isSupportedToken(address _token) external view returns (bool); // function getPoolStake(address _operator, address _token) external view returns (uint256); diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 3e38153..231a1bd 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {IKalypsoStaking} from "./IKalypsoStaking.sol"; +import {IStakingPool} from "./IStakingPool.sol"; -interface ISymbioticStaking is IKalypsoStaking { +interface ISymbioticStaking is IStakingPool { // function stakeOf(address _operator, address _token) external view returns (uint256); struct SnapshotTxCountInfo { diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 149ffa0..8e51d8f 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -11,7 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; -import {IKalypsoStaking} from "../../interfaces/staking/IKalypsoStaking.sol"; +import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; contract StakingManager is ContextUpgradeable, @@ -73,13 +73,13 @@ contract StakingManager is address pool = stakingPoolSet.at(i); if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled - IKalypsoStaking(pool).lockStake(_jobId, _operator); + IStakingPool(pool).lockStake(_jobId, _operator); } } // TODO // function getPoolStake(address _pool, address _operator, address _token) internal view returns (uint256) { - // return IKalypsoStaking(_pool).getStakeAmount(_operator, _token); + // return IStakingPool(_pool).getStakeAmount(_operator, _token); // } // called when job is completed to unlock the locked stakes @@ -89,7 +89,7 @@ contract StakingManager is for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - IKalypsoStaking(pool).unlockStake(_jobId); + IStakingPool(pool).unlockStake(_jobId); } // TODO: emit event From 193a86b7f2d2a567040f568635529560e2a1eaf9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 25 Sep 2024 23:59:53 +0900 Subject: [PATCH 073/158] init jobManager --- contracts/staking/l2_contracts/StakingManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 8e51d8f..00c90d8 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -51,7 +51,7 @@ contract StakingManager is _; } - function initialize(address _admin) public initializer { + function initialize(address _admin, address _jobManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -59,7 +59,7 @@ contract StakingManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); - // TODO: set jobmanager + jobManager = _jobManager; } // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) From a3d605e7b3167abeb7b1bff4c9d821b5f52fe67f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:00:35 +0900 Subject: [PATCH 074/158] remove unused struct --- contracts/staking/l2_contracts/StakingManager.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 00c90d8..7066e7a 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -40,12 +40,6 @@ contract StakingManager is bool enabled; } - // operator, lockToken, lockAmount should be stored in JobManager - struct StakeLockInfo { - uint256 totalLockAmount; - uint256 unlockTimestamp; - } - modifier onlyJobManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); _; From 1f4afc1b5d3d1c20a3610f108d9e0e885213be99 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:02:18 +0900 Subject: [PATCH 075/158] remove minStake from StakingManager --- contracts/staking/l2_contracts/StakingManager.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 7066e7a..e599002 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -36,7 +36,6 @@ contract StakingManager is uint256 stakeDataTransmitterShare; struct PoolConfig { uint256 weight; - uint256 minStake; // min stake for bool enabled; } From dac7f805568ff7574e41844009f8c29ddec53718 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:04:54 +0900 Subject: [PATCH 076/158] remove isEnabledPool mapping --- contracts/staking/l2_contracts/StakingManager.sol | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index e599002..08d920a 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -28,7 +28,6 @@ contract StakingManager is EnumerableSet.AddressSet private stakingPoolSet; - mapping(address pool => bool isEnabled) private stakingPoolStatus; mapping(address pool => uint256 weight) private stakingPoolWeight; mapping(address pool => PoolConfig config) private poolConfig; @@ -117,11 +116,6 @@ contract StakingManager is // TODO: onlyAdmin } - function setStakingPoolStatus(address _stakingPool, bool _status) external { - // TODO: onlyAdmin - } - - // TODO: integration with JobManager function setJobManager(address _jobManager) external { // TODO: only admin @@ -144,8 +138,7 @@ contract StakingManager is } sum += _transmitterShare; - // as the weight is in percentage, the sum of the shares should be 10000 - // TODO: sum of enabled pools should be 10000 + // as the weight is in percentage, the sum of the shares should be 1e18 require(sum == 1e18, "Invalid Shares"); for(uint256 i = 0; i < _pools.length; i++) { From 2262916457bfd008b5a7cd145e61b9bc7c16ac33 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:09:12 +0900 Subject: [PATCH 077/158] remove unlockEpoch --- contracts/staking/l2_contracts/StakingManager.sol | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 08d920a..b0c8e08 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -10,7 +10,7 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; contract StakingManager is @@ -19,7 +19,7 @@ contract StakingManager is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable - // INativeStaking + // IStakingManager // TODO { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -31,7 +31,6 @@ contract StakingManager is mapping(address pool => uint256 weight) private stakingPoolWeight; mapping(address pool => PoolConfig config) private poolConfig; - uint256 unlockEpoch; uint256 stakeDataTransmitterShare; struct PoolConfig { uint256 weight; @@ -121,12 +120,6 @@ contract StakingManager is // TODO: only admin } - // TODO: check if needed - function setUnlockEpoch(uint256 _unlockEpoch) external { - // TODO: check if the unlockEpoch is longer than the proofDeadline - } - - // when job is closed, the reward will be distributed based on the share function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external { // TODO: only admin From 8d86dd3a5a122dc8783960b2598aa5452d8cd943 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:13:14 +0900 Subject: [PATCH 078/158] fix submitProof logic --- contracts/staking/l2_contracts/JobManager.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 4405bd6..68757fd 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -38,6 +38,8 @@ contract JobManager { */ function submitProof(uint256 jobId, bytes calldata proof) public { _verifyProof(jobId, proof); + + stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake } /** @@ -50,9 +52,11 @@ contract JobManager { uint256 len = jobIds.length; for (uint256 idx = 0; idx < len; idx++) { - _verifyProof(jobIds[idx], proofs[idx]); + uint256 jobId = jobIds[idx]; + + _verifyProof(jobId, proofs[idx]); - uint256 jobId; + // TODO: let onJobCompletion also accept array of jobIds stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake } From ffea6b6c26a35a50e134b467df1c2fcb51a7f2c0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 00:13:52 +0900 Subject: [PATCH 079/158] remove operatorSet --- contracts/staking/l2_contracts/NativeStaking.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index f46ed3e..4fbd0aa 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -25,7 +25,6 @@ contract NativeStaking is using SafeERC20 for IERC20; EnumerableSet.AddressSet private tokenSet; - EnumerableSet.AddressSet private operatorSet; // TODO: check if needed address public nativeStakingReward; address public stakingManager; From 245beb7bd020c8f96aeaaa828b0e808f2db14c08 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:10:50 +0900 Subject: [PATCH 080/158] fix lockStake logic --- .../staking/l2_contracts/NativeStaking.sol | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 4fbd0aa..f903af2 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -45,10 +45,9 @@ contract NativeStaking is mapping(address token => uint256 amount) public totalStakedAmounts; /* Locked Stakes */ - // TODO: check if mappings below are needed mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; // includes selfStake and delegatedStake amount - mapping(address token => uint256 amount) public totalLockedAmounts; + // mapping(address token => uint256 amount) public totalLockedAmounts; // TODO: delete struct NativeStakingLock { address token; @@ -147,8 +146,13 @@ contract NativeStaking is return totalStakedAmounts[_token]; } - function getActiveStakeAmount(address _token) external view returns (uint256) { - return totalStakedAmounts[_token] - totalLockedAmounts[_token]; + // TODO: check if needed + // function getActiveStakeAmount(address _token) public view returns (uint256) { + // return totalStakedAmounts[_token] - totalLockedAmounts[_token]; + // } + + function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; } function isSupportedToken(address _token) external view returns (bool) { @@ -177,12 +181,12 @@ contract NativeStaking is function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); uint256 _amountToLock = amountToLock[_token]; - require(operatorStakedAmounts[_operator][_token] >= _amountToLock, "Insufficient stake to lock"); + require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; - totalLockedAmounts[_token] += _amountToLock; + // totalLockedAmounts[_token] += _amountToLock; // TODO: delete // TODO: emit event } @@ -201,8 +205,8 @@ contract NativeStaking is function unlockStake(uint256 _jobId) external onlyStakingManager { // TODO: consider the case when new pool is added during job - // TODO: need to mark something that indicates that job is completed? jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); + // TODO: should "jobId => operator" data be pulled from JobManager to update operatorLockedAmounts? // TODO: distribute reward From d2652227254bf5faf18dd91fbcc014cf3ff6721f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:13:33 +0900 Subject: [PATCH 081/158] fix account to msg.sender --- contracts/staking/l2_contracts/NativeStaking.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index f903af2..5d0443e 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -79,20 +79,20 @@ contract NativeStaking is } // Staker should be able to choose an Operator they want to stake into - function stake(address _account, address _operator, address _token, uint256 _amount) + function stake(address _operator, address _token, uint256 _amount) external onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { - IERC20(_token).safeTransferFrom(_account, address(this), _amount); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); - stakedAmounts[_account][_operator][_token] += _amount; + stakedAmounts[msg.sender][_operator][_token] += _amount; operatorStakedAmounts[_operator][_token] += _amount; // NativeStakingReward contract will read staking amount info from this contract // and update reward related states - INativeStakingReward(nativeStakingReward).update(_account, _token, _operator); + INativeStakingReward(nativeStakingReward).update(msg.sender, _token, _operator); emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -179,7 +179,7 @@ contract NativeStaking is /*======================================== StakingManager ========================================*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectLockToken(); + address _token = _selectTokenToLock(); uint256 _amountToLock = amountToLock[_token]; require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); @@ -191,7 +191,7 @@ contract NativeStaking is // TODO: emit event } - function _selectLockToken() internal view returns(address) { + function _selectTokenToLock() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); uint256 idx; From 819c13fe1c48511e26350bd0a03d35e1670ba85a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:15:32 +0900 Subject: [PATCH 082/158] remove selfstake --- .../staking/l2_contracts/NativeStaking.sol | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 5d0443e..ffd09a0 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -85,6 +85,10 @@ contract NativeStaking is onlySupportedToken(_token) nonReentrant { + // this check can be removed in the future to allow delegatedStake + // TODO: check if _operator is an actual _operator address? + require(msg.sender == _operator, "Only operator can stake"); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); stakedAmounts[msg.sender][_operator][_token] += _amount; @@ -97,21 +101,6 @@ contract NativeStaking is emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } - // Operators need to self stake tokenSet to be able to receive jobs (jobs will be restricted based on self stake amount) - // This should update StakingManger's state - function operatorSelfStake(address _operator, address _token, uint256 _amount) - external - onlySupportedSignature(msg.sig) - onlySupportedToken(_token) - nonReentrant - { - IERC20(_token).safeTransferFrom(_operator, address(this), _amount); - - operatorStakedAmounts[_operator][_token] += _amount; - - emit SelfStaked(_operator, _token, _amount, block.timestamp); - } - // This should update StakingManger's state function withdrawStake(address _account, address _operator, address _token, uint256 _amount) external nonReentrant { require(stakedAmounts[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); @@ -130,16 +119,6 @@ contract NativeStaking is emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); } - function withdrawSelfStake(address operator, address token, uint256 amount) external nonReentrant { - require(operatorStakedAmounts[operator][token] >= amount, "Insufficient selfstake"); - - IERC20(token).safeTransfer(operator, amount); - - operatorStakedAmounts[operator][token] -= amount; - - emit SelfStakeWithdrawn(operator, token, amount, block.timestamp); - } - /*======================================== Getters ========================================*/ function getStakeAmount(address _token) external view returns (uint256) { From f31ae902cd8be8c03cb579669002eafeb5ee6843 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:16:34 +0900 Subject: [PATCH 083/158] remove account param --- contracts/staking/l2_contracts/NativeStaking.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index ffd09a0..07c648f 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -39,7 +39,7 @@ contract NativeStaking is /* Stake */ // total staked amounts for each operator, includes selfStake and delegatedStake amount mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; - // selfstake if account == operator + // staked amount for each account mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; // total staked amount for each token mapping(address token => uint256 amount) public totalStakedAmounts; @@ -102,7 +102,7 @@ contract NativeStaking is } // This should update StakingManger's state - function withdrawStake(address _account, address _operator, address _token, uint256 _amount) external nonReentrant { + function withdrawStake(address _operator, address _token, uint256 _amount) external nonReentrant { require(stakedAmounts[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); // TODO: check locked time @@ -114,7 +114,7 @@ contract NativeStaking is stakedAmounts[msg.sender][_operator][_token] -= _amount; operatorStakedAmounts[_operator][_token] -= _amount; - INativeStakingReward(nativeStakingReward).update(_account, _token, _operator); + INativeStakingReward(nativeStakingReward).update(msg.sender, _token, _operator); emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); } From 2dce13058fd9ff131632b0d103296748eac57ec0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:18:38 +0900 Subject: [PATCH 084/158] withdraw checks operatorActiveStakeAmount --- contracts/staking/l2_contracts/NativeStaking.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 07c648f..2626418 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -103,7 +103,7 @@ contract NativeStaking is // This should update StakingManger's state function withdrawStake(address _operator, address _token, uint256 _amount) external nonReentrant { - require(stakedAmounts[msg.sender][_operator][_token] >= _amount, "Insufficient stake"); + require(getOperatorActiveStakeAmount(_operator, _token) >= _amount, "Insufficient stake"); // TODO: check locked time From 31953258d6f0561100ed957f04d12025c50f4e81 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 15:19:41 +0900 Subject: [PATCH 085/158] add todo comment --- contracts/staking/l2_contracts/NativeStaking.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 2626418..a7ad1d8 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -186,6 +186,7 @@ contract NativeStaking is jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); // TODO: should "jobId => operator" data be pulled from JobManager to update operatorLockedAmounts? + // TODO: totalLockedAmounts // TODO: distribute reward From fae46b16cbaa23a005821e8b310789befbd87f91 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 19:43:31 +0900 Subject: [PATCH 086/158] remove minStakeAmount and fix typo --- contracts/interfaces/staking/INativeStaking.sol | 2 -- contracts/interfaces/staking/ISymbioticStaking.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 2 +- contracts/staking/l2_contracts/NativeStaking.sol | 5 ++--- contracts/staking/l2_contracts/SymbioticStaking.sol | 9 ++++++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index b19e7ec..3a406f0 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -7,9 +7,7 @@ interface INativeStaking is IStakingPool { // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - event SelfStaked(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event StakeWithdrawn(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - event SelfStakeWithdrawn(address indexed operator, address indexed token, uint256 amount, uint256 timestamp); // function stakeOf(address _operator, address _token) external view returns (uint256); diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 231a1bd..c69cee7 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -29,7 +29,7 @@ interface ISymbioticStaking is IStakingPool { } struct ConfirmedTimestamp { - uint256 capturedTimestamp; + uint256 captureTimestamp; uint256 rewardShare; // TODO address transmitter; } diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 68757fd..63fd1ff 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -17,7 +17,7 @@ contract JobManager { address lockToken; uint256 lockedAmount; // this will go to slasher if the proof is not submitted before deadline uint256 deadline; - address dataTransmitter; // + address dataTransmitter; } mapping(uint256 => JobInfo) public jobs; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index a7ad1d8..2e16941 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -32,12 +32,11 @@ contract NativeStaking is /*======================================== Config ========================================*/ /* Config */ - mapping(address token => uint256 minStakeamount) public minStakeAmount; mapping(address token => uint256 lockAmount) public amountToLock; mapping(bytes4 sig => bool isSupported) private supportedSignatures; /* Stake */ - // total staked amounts for each operator, includes selfStake and delegatedStake amount + // total staked amounts for each operator mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; // staked amount for each account mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; @@ -46,7 +45,7 @@ contract NativeStaking is /* Locked Stakes */ mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; - mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; // includes selfStake and delegatedStake amount + mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; // mapping(address token => uint256 amount) public totalLockedAmounts; // TODO: delete struct NativeStakingLock { diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 92d0fb3..ec02eca 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -21,7 +21,6 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); /* Config */ - mapping(address token => uint256 amount) public minStakeAmount; mapping(address token => uint256 amount) public amountToLock; EnumerableSet.AddressSet tokenSet; @@ -116,7 +115,7 @@ contract SymbioticStaking is ISymbioticStaking { function lockStake(uint256 _jobId, address _operator) external { address _token = _selectLockToken(); uint256 stakedAmount = getOperatorStake(_operator, _token); - require(stakedAmount >= minStakeAmount[_token], "Insufficient stake amount"); + require(stakedAmount >= amountToLock[_token], "Insufficient stake amount"); // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; @@ -233,7 +232,11 @@ contract SymbioticStaking is ISymbioticStaking { confirmedTimestamps.push(confirmedTimestamp); // TODO: calculate rewards for the transmitter based on transmitterComission + // TODO: Data transmitter should get transmitterComission% of the rewards + + + // TODO: "transmitterComission" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + submissionCooldown)" // TODO: emit SubmissionCompleted @@ -242,7 +245,7 @@ contract SymbioticStaking is ISymbioticStaking { /*======================================== Getters ========================================*/ function lastConfirmedTimestamp() public view returns (uint256) { - return confirmedTimestamps[confirmedTimestamps.length - 1].capturedTimestamp; + return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; } function isSupportedToken(address _token) public view returns (bool) { From 948ea0e3487dabcafe2fad44df2fbc54dacbb406 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 19:50:59 +0900 Subject: [PATCH 087/158] remove locked token address from StakingManager --- contracts/interfaces/staking/IStakingManager.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 11 ++++++----- contracts/staking/l2_contracts/SymbioticStaking.sol | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 9f0aa53..9247aad 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; - function onJobCompletion(uint256 jobId, address token) external; + function onJobCompletion(uint256 jobId) external; function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external; diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 63fd1ff..9b8842a 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -14,10 +14,7 @@ contract JobManager { struct JobInfo { address operator; - address lockToken; - uint256 lockedAmount; // this will go to slasher if the proof is not submitted before deadline uint256 deadline; - address dataTransmitter; } mapping(uint256 => JobInfo) public jobs; @@ -27,8 +24,12 @@ contract JobManager { // TODO: called only from Kalypso Protocol // TODO: create a job and record StakeData Transmitter who submitted capture timestamp - + jobs[jobId] = JobInfo({ + operator: operator, + deadline: block.timestamp + JOB_DURATION + }); + // TODO: call creation function in StakingManager stakingManager.onJobCreation(jobId, operator); // lock stake } @@ -39,7 +40,7 @@ contract JobManager { function submitProof(uint256 jobId, bytes calldata proof) public { _verifyProof(jobId, proof); - stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake + stakingManager.onJobCompletion(jobId); // unlock stake } /** diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index ec02eca..b223edf 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -41,7 +41,8 @@ contract SymbioticStaking is ISymbioticStaking { struct SymbioticStakingLock { address token; uint256 amount; - address transmitter; + // transmitter who submitted with confirmedTimestamp used when job is created + address transmitter; } /* Staking */ From e60d824f07e1720e02363335811936129c6e9883 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 20:33:27 +0900 Subject: [PATCH 088/158] add deadline logic --- contracts/staking/l2_contracts/JobManager.sol | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 9b8842a..c17d9b7 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -23,21 +23,22 @@ contract JobManager { function createJob(uint256 jobId, address operator) external { // TODO: called only from Kalypso Protocol - // TODO: create a job and record StakeData Transmitter who submitted capture timestamp - + // stakeToken and lockAmount will be decided in each pool jobs[jobId] = JobInfo({ operator: operator, deadline: block.timestamp + JOB_DURATION }); // TODO: call creation function in StakingManager - stakingManager.onJobCreation(jobId, operator); // lock stake + stakingManager.onJobCreation(jobId, operator); } /** * @notice Submit Single Proof */ function submitProof(uint256 jobId, bytes calldata proof) public { + require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); + _verifyProof(jobId, proof); stakingManager.onJobCompletion(jobId); // unlock stake @@ -54,15 +55,26 @@ contract JobManager { uint256 len = jobIds.length; for (uint256 idx = 0; idx < len; idx++) { uint256 jobId = jobIds[idx]; + require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); _verifyProof(jobId, proofs[idx]); // TODO: let onJobCompletion also accept array of jobIds - stakingManager.onJobCompletion(jobId, jobs[jobId].lockToken); // unlock stake + stakingManager.onJobCompletion(jobId); // unlock stake } } + function refundFee(uint256 jobId) external { + require(block.timestamp > jobs[jobId].deadline, "Job not Expired"); + + // TODO: refund fee + + // TODO: emit event + } + + // TODO: implement manual slash + function _verifyProof(uint256 jobId, bytes calldata proof) internal { // TODO: verify proof } From 2575a1fff3c28481bfc7265020af799f6cbcd3d5 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 20:36:47 +0900 Subject: [PATCH 089/158] remove supported signature --- contracts/interfaces/staking/INativeStaking.sol | 2 -- contracts/staking/l2_contracts/NativeStaking.sol | 14 -------------- 2 files changed, 16 deletions(-) diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 3a406f0..166989e 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -12,6 +12,4 @@ interface INativeStaking is IStakingPool { // function stakeOf(address _operator, address _token) external view returns (uint256); // function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); - - // function isSupportedSignature(bytes4 sig) external view returns (bool); } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 2e16941..0f5bdb0 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -33,7 +33,6 @@ contract NativeStaking is /* Config */ mapping(address token => uint256 lockAmount) public amountToLock; - mapping(bytes4 sig => bool isSupported) private supportedSignatures; /* Stake */ // total staked amounts for each operator @@ -58,11 +57,6 @@ contract NativeStaking is _; } - modifier onlySupportedSignature(bytes4 sig) { - require(supportedSignatures[sig], "Function not supported"); - _; - } - modifier onlyStakingManager() { require(msg.sender == stakingManager, "Only StakingManager"); _; @@ -80,7 +74,6 @@ contract NativeStaking is // Staker should be able to choose an Operator they want to stake into function stake(address _operator, address _token, uint256 _amount) external - onlySupportedSignature(msg.sig) onlySupportedToken(_token) nonReentrant { @@ -137,9 +130,6 @@ contract NativeStaking is return tokenSet.contains(_token); } - function isSupportedSignature(bytes4 sig) external view returns (bool) { - return supportedSignatures[sig]; - } /*======================================== Admin ========================================*/ @@ -151,10 +141,6 @@ contract NativeStaking is require(tokenSet.remove(token), "Token does not exist"); } - function setSupportedSignature(bytes4 sig, bool isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { - supportedSignatures[sig] = isSupported; - } - /*======================================== StakingManager ========================================*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectTokenToLock(); From cd478920e527b300e3c722ea63f7f08617466cb9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 22:32:24 +0900 Subject: [PATCH 090/158] add refundFee --- contracts/staking/l2_contracts/JobManager.sol | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index c17d9b7..cf5c176 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; /* @@ -8,29 +11,38 @@ import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. */ contract JobManager { - uint256 constant JOB_DURATION = 1 days; + using SafeERC20 for IERC20; - IStakingManager stakingManager; + address public stakingManager; + address public feeToken; + uint256 public jobDuration = 1 days; struct JobInfo { + address requester; address operator; + uint256 feePaid; uint256 deadline; } - mapping(uint256 => JobInfo) public jobs; + mapping(uint256 jobId => JobInfo jobInfo) public jobs; + uint256 feePaid; // TODO: check paramter for job details - function createJob(uint256 jobId, address operator) external { + function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external { // TODO: called only from Kalypso Protocol + + IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); // stakeToken and lockAmount will be decided in each pool - jobs[jobId] = JobInfo({ - operator: operator, - deadline: block.timestamp + JOB_DURATION + jobs[_jobId] = JobInfo({ + requester: _requester, + operator: _operator, + feePaid: _feeAmount, + deadline: block.timestamp + jobDuration }); // TODO: call creation function in StakingManager - stakingManager.onJobCreation(jobId, operator); + IStakingManager(stakingManager).onJobCreation(_jobId, _operator); } /** @@ -41,7 +53,7 @@ contract JobManager { _verifyProof(jobId, proof); - stakingManager.onJobCompletion(jobId); // unlock stake + IStakingManager(stakingManager).onJobCompletion(jobId); // unlock stake } /** @@ -60,26 +72,27 @@ contract JobManager { _verifyProof(jobId, proofs[idx]); // TODO: let onJobCompletion also accept array of jobIds - stakingManager.onJobCompletion(jobId); // unlock stake + IStakingManager(stakingManager).onJobCompletion(jobId); // unlock stake } } function refundFee(uint256 jobId) external { require(block.timestamp > jobs[jobId].deadline, "Job not Expired"); + require(jobs[jobId].requester == msg.sender, "Not Requester"); // TODO: refund fee + jobs[jobId].feePaid = 0; + IERC20(feeToken).safeTransfer(jobs[jobId].requester, jobs[jobId].feePaid); // TODO: emit event } - // TODO: implement manual slash - function _verifyProof(uint256 jobId, bytes calldata proof) internal { // TODO: verify proof } function setStakingManager(address _stakingManager) external { - stakingManager = IStakingManager(_stakingManager); + stakingManager = _stakingManager; } } \ No newline at end of file From 22049582b816dcafb8bcd3ec397a7c1d552421e4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 22:36:02 +0900 Subject: [PATCH 091/158] add upgradeability --- contracts/interfaces/staking/IJobManager.sol | 7 +++ contracts/staking/l2_contracts/JobManager.sol | 51 ++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 contracts/interfaces/staking/IJobManager.sol diff --git a/contracts/interfaces/staking/IJobManager.sol b/contracts/interfaces/staking/IJobManager.sol new file mode 100644 index 0000000..963f683 --- /dev/null +++ b/contracts/interfaces/staking/IJobManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +interface IJobManager { + function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external; + function submitProof(uint256 jobId, bytes calldata proof) external; +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index cf5c176..c0296e3 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -1,16 +1,30 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; /* JobManager contract is responsible for creating and managing jobs. Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. */ -contract JobManager { +contract JobManager is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + IJobManager +{ using SafeERC20 for IERC20; address public stakingManager; @@ -27,8 +41,18 @@ contract JobManager { mapping(uint256 jobId => JobInfo jobInfo) public jobs; uint256 feePaid; + function initialize(address _admin) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + } + + // TODO: check paramter for job details - function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external { + function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external nonReentrant { // TODO: called only from Kalypso Protocol IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); @@ -48,7 +72,7 @@ contract JobManager { /** * @notice Submit Single Proof */ - function submitProof(uint256 jobId, bytes calldata proof) public { + function submitProof(uint256 jobId, bytes calldata proof) public nonReentrant { require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); _verifyProof(jobId, proof); @@ -59,7 +83,7 @@ contract JobManager { /** * @notice Submit Multiple proofs in single transaction */ - function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external { + function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external nonReentrant { require(jobIds.length == proofs.length, "Invalid Length"); // TODO: close job and distribute rewards @@ -77,7 +101,7 @@ contract JobManager { } - function refundFee(uint256 jobId) external { + function refundFee(uint256 jobId) external nonReentrant { require(block.timestamp > jobs[jobId].deadline, "Job not Expired"); require(jobs[jobId].requester == msg.sender, "Not Requester"); @@ -95,4 +119,19 @@ contract JobManager { function setStakingManager(address _stakingManager) external { stakingManager = _stakingManager; } + + + /*======================================== Overrides ========================================*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} } \ No newline at end of file From 8c2cbe099ea1df969b4066b55e05b10c84801e98 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 22:40:20 +0900 Subject: [PATCH 092/158] fix param variable --- contracts/staking/l2_contracts/JobManager.sol | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index c0296e3..3a9a55d 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -27,10 +27,6 @@ contract JobManager is { using SafeERC20 for IERC20; - address public stakingManager; - address public feeToken; - uint256 public jobDuration = 1 days; - struct JobInfo { address requester; address operator; @@ -39,22 +35,29 @@ contract JobManager is } mapping(uint256 jobId => JobInfo jobInfo) public jobs; - uint256 feePaid; - function initialize(address _admin) public initializer { + address public stakingManager; + address public feeToken; + + uint256 public jobDuration; + uint256 public totalFeeStored; // TODO: check if needed + + function initialize(address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); __UUPSUpgradeable_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + stakingManager = _stakingManager; + feeToken = _feeToken; + jobDuration = _jobDuration; } // TODO: check paramter for job details function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external nonReentrant { - // TODO: called only from Kalypso Protocol - IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); // stakeToken and lockAmount will be decided in each pool @@ -65,35 +68,38 @@ contract JobManager is deadline: block.timestamp + jobDuration }); - // TODO: call creation function in StakingManager IStakingManager(stakingManager).onJobCreation(_jobId, _operator); + + totalFeeStored += _feeAmount; + + // TODO: emit event } /** * @notice Submit Single Proof */ - function submitProof(uint256 jobId, bytes calldata proof) public nonReentrant { - require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); + function submitProof(uint256 _jobId, bytes calldata _proof) public nonReentrant { + require(block.timestamp <= jobs[_jobId].deadline, "Job Expired"); - _verifyProof(jobId, proof); + _verifyProof(_jobId, _proof); - IStakingManager(stakingManager).onJobCompletion(jobId); // unlock stake + IStakingManager(stakingManager).onJobCompletion(_jobId); // unlock stake } /** * @notice Submit Multiple proofs in single transaction */ - function submitProofs(uint256[] calldata jobIds, bytes[] calldata proofs) external nonReentrant { - require(jobIds.length == proofs.length, "Invalid Length"); + function submitProofs(uint256[] calldata _jobIds, bytes[] calldata _proofs) external nonReentrant { + require(_jobIds.length == _proofs.length, "Invalid Length"); // TODO: close job and distribute rewards - uint256 len = jobIds.length; + uint256 len = _jobIds.length; for (uint256 idx = 0; idx < len; idx++) { - uint256 jobId = jobIds[idx]; + uint256 jobId = _jobIds[idx]; require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); - _verifyProof(jobId, proofs[idx]); + _verifyProof(jobId, _proofs[idx]); // TODO: let onJobCompletion also accept array of jobIds IStakingManager(stakingManager).onJobCompletion(jobId); // unlock stake @@ -101,19 +107,23 @@ contract JobManager is } - function refundFee(uint256 jobId) external nonReentrant { - require(block.timestamp > jobs[jobId].deadline, "Job not Expired"); - require(jobs[jobId].requester == msg.sender, "Not Requester"); + function refundFee(uint256 _jobId) external nonReentrant { + require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); + require(jobs[_jobId].requester == msg.sender, "Not Requester"); // TODO: refund fee - jobs[jobId].feePaid = 0; + jobs[_jobId].feePaid = 0; + totalFeeStored -= jobs[_jobId].feePaid; - IERC20(feeToken).safeTransfer(jobs[jobId].requester, jobs[jobId].feePaid); + IERC20(feeToken).safeTransfer(jobs[_jobId].requester, jobs[_jobId].feePaid); + // TODO: emit event } - function _verifyProof(uint256 jobId, bytes calldata proof) internal { + function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { // TODO: verify proof + + // TODO: emit event } function setStakingManager(address _stakingManager) external { From c9341232fa1279184abd14d8986b11be3e970e32 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:15:15 +0900 Subject: [PATCH 093/158] resolve TODOs for StakingManager --- .../staking/l2_contracts/StakingManager.sol | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index b0c8e08..9e8e100 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -18,8 +18,8 @@ contract StakingManager is ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, - ReentrancyGuardUpgradeable - // IStakingManager // TODO + ReentrancyGuardUpgradeable, + IStakingManager { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -37,6 +37,11 @@ contract StakingManager is bool enabled; } + modifier onlyAdmin() { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); + _; + } + modifier onlyJobManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); _; @@ -68,14 +73,8 @@ contract StakingManager is } } - // TODO - // function getPoolStake(address _pool, address _operator, address _token) internal view returns (uint256) { - // return IStakingPool(_pool).getStakeAmount(_operator, _token); - // } - // called when job is completed to unlock the locked stakes function onJobCompletion(uint256 _jobId) external onlyJobManager { - // TODO: unlock the locked stakes uint256 len = stakingPoolSet.length(); for(uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); @@ -87,43 +86,35 @@ contract StakingManager is } /*======================================== Getters ========================================*/ - - // check if the job is slashable and can be sent to the slashing manager - // this only tells if the deadline for proof submission has passed - // so even when this function returns true and transaction submitted to L1 can be reverted - // when someone already has submitted the proof - function isSlashable(address _jobId) external view returns (bool) { - // TODO: check if the proof was submitted before the deadline, so need to query jobmanager - } - function isEnabledPool(address _pool) public view returns (bool) { return poolConfig[_pool].enabled; } - /*======================================== Getter for Staking ========================================*/ - - - /*======================================== Admin ========================================*/ // add new staking pool - function addStakingPool(address _stakingPool) external { - // TODO: onlyAdmin + function addStakingPool(address _stakingPool) external onlyAdmin { + stakingPoolSet.add(_stakingPool); + + // TODO: emit event } - function removeStakingPool(address _stakingPool) external { - // TODO: onlyAdmin + function removeStakingPool(address _stakingPool) external onlyAdmin { + stakingPoolSet.remove(_stakingPool); + + // TODO: emit event } // TODO: integration with JobManager - function setJobManager(address _jobManager) external { - // TODO: only admin + function setJobManager(address _jobManager) external onlyAdmin { + jobManager = _jobManager; + + // TODO: emit event } // when job is closed, the reward will be distributed based on the share - function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external { - // TODO: only admin - require(_pools.length == _shares.length, "Invalid Length"); + function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external onlyAdmin { + require(_pools.length == _shares.length || _pools.length == stakingPoolSet.length(), "Invalid Length"); uint256 sum = 0; for(uint256 i = 0; i < _shares.length; i++) { From 683e4297265f6d5c9c4b51e4f21904e5ff23dc12 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:18:12 +0900 Subject: [PATCH 094/158] remove onlyAdmin --- .../staking/l2_contracts/StakingManager.sol | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 9e8e100..2f829d6 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -30,18 +30,14 @@ contract StakingManager is mapping(address pool => uint256 weight) private stakingPoolWeight; mapping(address pool => PoolConfig config) private poolConfig; - + uint256 stakeDataTransmitterShare; + struct PoolConfig { uint256 weight; bool enabled; } - modifier onlyAdmin() { - require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); - _; - } - modifier onlyJobManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); _; @@ -64,10 +60,10 @@ contract StakingManager is // note: data related to the job should be stored in JobManager (e.g. operator, lockToken, lockAmount, proofDeadline) function onJobCreation(uint256 _jobId, address _operator) external onlyJobManager { uint256 len = stakingPoolSet.length(); - - for(uint256 i = 0; i < len; i++) { + + for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - if(!isEnabledPool(pool)) continue; // skip if the pool is not enabled + if (!isEnabledPool(pool)) continue; // skip if the pool is not enabled IStakingPool(pool).lockStake(_jobId, _operator); } @@ -76,9 +72,9 @@ contract StakingManager is // called when job is completed to unlock the locked stakes function onJobCompletion(uint256 _jobId) external onlyJobManager { uint256 len = stakingPoolSet.length(); - for(uint256 i = 0; i < len; i++) { + for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - + IStakingPool(pool).unlockStake(_jobId); } @@ -93,31 +89,34 @@ contract StakingManager is /*======================================== Admin ========================================*/ // add new staking pool - function addStakingPool(address _stakingPool) external onlyAdmin { + function addStakingPool(address _stakingPool) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingPoolSet.add(_stakingPool); // TODO: emit event } - function removeStakingPool(address _stakingPool) external onlyAdmin { + function removeStakingPool(address _stakingPool) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingPoolSet.remove(_stakingPool); // TODO: emit event } // TODO: integration with JobManager - function setJobManager(address _jobManager) external onlyAdmin { + function setJobManager(address _jobManager) external onlyRole(DEFAULT_ADMIN_ROLE) { jobManager = _jobManager; // TODO: emit event } // when job is closed, the reward will be distributed based on the share - function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) external onlyAdmin { + function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { require(_pools.length == _shares.length || _pools.length == stakingPoolSet.length(), "Invalid Length"); uint256 sum = 0; - for(uint256 i = 0; i < _shares.length; i++) { + for (uint256 i = 0; i < _shares.length; i++) { sum += _shares[i]; } sum += _transmitterShare; @@ -125,7 +124,7 @@ contract StakingManager is // as the weight is in percentage, the sum of the shares should be 1e18 require(sum == 1e18, "Invalid Shares"); - for(uint256 i = 0; i < _pools.length; i++) { + for (uint256 i = 0; i < _pools.length; i++) { poolConfig[_pools[i]].weight = _shares[i]; } stakeDataTransmitterShare = _transmitterShare; From f4df6e1eca25e99b36d22dd0c2f9ab69bb838a83 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:26:29 +0900 Subject: [PATCH 095/158] add missing admin functions --- .../staking/l2_contracts/NativeStaking.sol | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 0f5bdb0..dd8b90c 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -24,13 +24,17 @@ contract NativeStaking is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; + struct NativeStakingLock { + address token; + uint256 amount; + } + + EnumerableSet.AddressSet private tokenSet; address public nativeStakingReward; address public stakingManager; - /*======================================== Config ========================================*/ - /* Config */ mapping(address token => uint256 lockAmount) public amountToLock; @@ -39,18 +43,10 @@ contract NativeStaking is mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; // staked amount for each account mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; - // total staked amount for each token - mapping(address token => uint256 amount) public totalStakedAmounts; /* Locked Stakes */ mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; - // mapping(address token => uint256 amount) public totalLockedAmounts; // TODO: delete - - struct NativeStakingLock { - address token; - uint256 amount; - } modifier onlySupportedToken(address _token) { require(tokenSet.contains(_token), "Token not supported"); @@ -77,8 +73,8 @@ contract NativeStaking is onlySupportedToken(_token) nonReentrant { - // this check can be removed in the future to allow delegatedStake // TODO: check if _operator is an actual _operator address? + // this check can be removed in the future to allow delegatedStake require(msg.sender == _operator, "Only operator can stake"); IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); @@ -94,33 +90,25 @@ contract NativeStaking is } // This should update StakingManger's state - function withdrawStake(address _operator, address _token, uint256 _amount) external nonReentrant { + function requestStakeWithdrawal(address _operator, address _token, uint256 _amount) external nonReentrant { require(getOperatorActiveStakeAmount(_operator, _token) >= _amount, "Insufficient stake"); - // TODO: check locked time - - // TODO: read from staking manager and calculate withdrawable amount - - IERC20(_token).safeTransfer(msg.sender, _amount); - stakedAmounts[msg.sender][_operator][_token] -= _amount; operatorStakedAmounts[_operator][_token] -= _amount; + IERC20(_token).safeTransfer(msg.sender, _amount); + INativeStakingReward(nativeStakingReward).update(msg.sender, _token, _operator); emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); } - /*======================================== Getters ========================================*/ - - function getStakeAmount(address _token) external view returns (uint256) { - return totalStakedAmounts[_token]; + function withdrawStake(address _operator, address _token) external nonReentrant { + uint256 _amount = stakedAmounts[msg.sender][_operator][_token]; + require(_amount > 0, "No stake to withdraw"); } - // TODO: check if needed - // function getActiveStakeAmount(address _token) public view returns (uint256) { - // return totalStakedAmounts[_token] - totalLockedAmounts[_token]; - // } + /*======================================== Getters ========================================*/ function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { return operatorStakedAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; @@ -135,10 +123,32 @@ contract NativeStaking is function addToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { require(tokenSet.add(token), "Token already exists"); + + // TODO: emit event } function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { require(tokenSet.remove(token), "Token does not exist"); + + // TODO: emit event + } + + function setNativeStakingReward(address _nativeStakingReward) external onlyRole(DEFAULT_ADMIN_ROLE) { + nativeStakingReward = _nativeStakingReward; + + // TODO: emit event + } + + function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { + stakingManager = _stakingManager; + + // TODO: emit event + } + + function setAmountToLock(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + amountToLock[_token] = _amount; + + // TODO: emit event } /*======================================== StakingManager ========================================*/ @@ -150,7 +160,6 @@ contract NativeStaking is // lock stake jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; - // totalLockedAmounts[_token] += _amountToLock; // TODO: delete // TODO: emit event } @@ -171,6 +180,7 @@ contract NativeStaking is jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); // TODO: should "jobId => operator" data be pulled from JobManager to update operatorLockedAmounts? + // TODO: totalLockedAmounts // TODO: distribute reward From a421c16133b1e0e4e48ed93c2cca14e9d0c17cc0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:28:39 +0900 Subject: [PATCH 096/158] remove unnecessary functions --- contracts/interfaces/staking/IStakingManager.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 9247aad..33ac19b 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -6,10 +6,4 @@ interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; function onJobCompletion(uint256 jobId) external; - - function submitProofs(uint256[] memory jobIds, bytes[] calldata proofs) external; - - function submitProof(uint256 jobId, bytes calldata proof) external; - - function setStakingManager(address _stakingManager) external; } \ No newline at end of file From f8422ec13d9e62e9861ae5c287a7227bf141a9e5 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:28:48 +0900 Subject: [PATCH 097/158] add admin function --- contracts/staking/l2_contracts/JobManager.sol | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 3a9a55d..fca9b2d 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -55,7 +55,6 @@ contract JobManager is jobDuration = _jobDuration; } - // TODO: check paramter for job details function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external nonReentrant { IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); @@ -126,10 +125,19 @@ contract JobManager is // TODO: emit event } + /*======================================== Admin ========================================*/ + function setStakingManager(address _stakingManager) external { stakingManager = _stakingManager; } + function setFeeToken(address _feeToken) external { + feeToken = _feeToken; + } + + function setJobDuration(uint256 _jobDuration) external { + jobDuration = _jobDuration; + } /*======================================== Overrides ========================================*/ From 96ffa9f01929258251d75ba51049db9a72461d6c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:47:03 +0900 Subject: [PATCH 098/158] refactor code --- contracts/staking/l2_contracts/NativeStaking.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index dd8b90c..fc09fd0 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; @@ -29,7 +30,6 @@ contract NativeStaking is uint256 amount; } - EnumerableSet.AddressSet private tokenSet; address public nativeStakingReward; From fb50a1c9b383831bc00248f2f42616a9c51cddc8 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:47:10 +0900 Subject: [PATCH 099/158] add missing admin functions --- contracts/staking/l2_contracts/JobManager.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index fca9b2d..72ca50d 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -127,15 +127,15 @@ contract JobManager is /*======================================== Admin ========================================*/ - function setStakingManager(address _stakingManager) external { + function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingManager = _stakingManager; } - function setFeeToken(address _feeToken) external { + function setFeeToken(address _feeToken) external onlyRole(DEFAULT_ADMIN_ROLE) { feeToken = _feeToken; } - function setJobDuration(uint256 _jobDuration) external { + function setJobDuration(uint256 _jobDuration) external onlyRole(DEFAULT_ADMIN_ROLE) { jobDuration = _jobDuration; } From 3c2e6faa8bb11211235c5202f03c054a202d69a4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:47:17 +0900 Subject: [PATCH 100/158] add upgradeability --- .../staking/l2_contracts/SymbioticStaking.sol | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index b223edf..022445c 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -1,13 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; + // TODO: vault => token info should be updated by the admin -contract SymbioticStaking is ISymbioticStaking { +contract SymbioticStaking is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + ISymbioticStaking + { using EnumerableSet for EnumerableSet.AddressSet; + struct SymbioticStakingLock { + address token; + uint256 amount; + // transmitter who submitted with confirmedTimestamp used when job is created + address transmitter; + } uint256 submissionCooldown; // 18 decimal (in seconds) uint256 baseTransmitterComission; // 18 decimal (in percentage) @@ -20,15 +40,14 @@ contract SymbioticStaking is ISymbioticStaking { bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); + EnumerableSet.AddressSet tokenSet; + /* Config */ mapping(address token => uint256 amount) public amountToLock; - EnumerableSet.AddressSet tokenSet; - /* Symbiotic Snapshot */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received - // staked amount for each operator mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; // staked amount for each vault @@ -38,12 +57,6 @@ contract SymbioticStaking is ISymbioticStaking { ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received - struct SymbioticStakingLock { - address token; - uint256 amount; - // transmitter who submitted with confirmedTimestamp used when job is created - address transmitter; - } /* Staking */ mapping(uint256 jobId => SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake @@ -258,7 +271,29 @@ contract SymbioticStaking is ISymbioticStaking { } /*======================================== Admin ========================================*/ - function setSupportedToken(address _token, bool _isSupported) external { + function setSupportedToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { // TODO } + + function setSubmissionCooldown(uint256 _submissionCooldown) external onlyRole(DEFAULT_ADMIN_ROLE) { + submissionCooldown = _submissionCooldown; + } + + function setBaseTransmitterComission(uint256 _baseTransmitterComission) external onlyRole(DEFAULT_ADMIN_ROLE) { + baseTransmitterComission = _baseTransmitterComission; + } + + /*======================================== Overrides ========================================*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} } From 2a2c04381a9a49ffcd9343f668e1e2de11fd3e2c Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:47:34 +0900 Subject: [PATCH 101/158] remove onlyAdmin --- .../l2_contracts/SymbioticStakingReward.sol | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 57a1f9c..e087448 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -70,20 +70,12 @@ contract SymbioticStakingReward is address public symbioticStaking; - // TODO: vault => claimAddress modifier onlySymbioticStaking() { require(_msgSender() == symbioticStaking, "Caller is not the staking manager"); _; } - - modifier onlyAdmin() { - require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin"); - _; - } - - //-------------------------------- Init start --------------------------------// // TODO: initialize contract addresses function initialize(address _admin, address _stakingManager) public initializer { @@ -228,7 +220,7 @@ contract SymbioticStakingReward is //-------------------------------- Update end --------------------------------// //-------------------------------- Admin start --------------------------------// - function setStakingManager(address _stakingManager) public onlyAdmin { + function setStakingManager(address _stakingManager) public onlyRole(DEFAULT_ADMIN_ROLE) { _setStakingManager(_stakingManager); } @@ -237,11 +229,11 @@ contract SymbioticStakingReward is // TODO: emit event } - function addRewardToken(address _rewardToken) external onlyAdmin { + function addRewardToken(address _rewardToken) external onlyRole(DEFAULT_ADMIN_ROLE) { _rewardTokenSet.add(_rewardToken); } - function removeRewardToken(address _rewardToken) external onlyAdmin { + function removeRewardToken(address _rewardToken) external onlyRole(DEFAULT_ADMIN_ROLE) { _rewardTokenSet.remove(_rewardToken); } From a04883ba6f258491455052b4487edce1c78e997b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 26 Sep 2024 23:54:05 +0900 Subject: [PATCH 102/158] remove transmitter from setShare --- contracts/staking/l2_contracts/StakingManager.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 2f829d6..39d8a15 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -101,7 +101,6 @@ contract StakingManager is // TODO: emit event } - // TODO: integration with JobManager function setJobManager(address _jobManager) external onlyRole(DEFAULT_ADMIN_ROLE) { jobManager = _jobManager; @@ -109,7 +108,7 @@ contract StakingManager is } // when job is closed, the reward will be distributed based on the share - function setShare(address[] calldata _pools, uint256[] calldata _shares, uint256 _transmitterShare) + function setShare(address[] calldata _pools, uint256[] calldata _shares) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -119,7 +118,6 @@ contract StakingManager is for (uint256 i = 0; i < _shares.length; i++) { sum += _shares[i]; } - sum += _transmitterShare; // as the weight is in percentage, the sum of the shares should be 1e18 require(sum == 1e18, "Invalid Shares"); @@ -127,7 +125,6 @@ contract StakingManager is for (uint256 i = 0; i < _pools.length; i++) { poolConfig[_pools[i]].weight = _shares[i]; } - stakeDataTransmitterShare = _transmitterShare; } /*======================================== Override ========================================*/ From a5a761d30f81043535596842dbd1303fd5a6fada Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 00:08:00 +0900 Subject: [PATCH 103/158] add unlockStake logic --- .../staking/l2_contracts/NativeStaking.sol | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index fc09fd0..3b19d4e 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -26,6 +26,7 @@ contract NativeStaking is using SafeERC20 for IERC20; struct NativeStakingLock { + address operator; address token; uint256 amount; } @@ -73,7 +74,6 @@ contract NativeStaking is onlySupportedToken(_token) nonReentrant { - // TODO: check if _operator is an actual _operator address? // this check can be removed in the future to allow delegatedStake require(msg.sender == _operator, "Only operator can stake"); @@ -158,12 +158,22 @@ contract NativeStaking is require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake - jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); + jobLockedAmounts[_jobId] = NativeStakingLock(_operator, _token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; // TODO: emit event } + function unlockStake(uint256 _jobId) external onlyStakingManager { + NativeStakingLock memory lock = jobLockedAmounts[_jobId]; + operatorLockedAmounts[lock.operator][lock.token] -= lock.amount; + delete jobLockedAmounts[_jobId]; + + // TODO: distribute reward + + // TODO: emit event + } + function _selectTokenToLock() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); @@ -175,20 +185,6 @@ contract NativeStaking is return tokenSet.at(idx); } - function unlockStake(uint256 _jobId) external onlyStakingManager { - // TODO: consider the case when new pool is added during job - - jobLockedAmounts[_jobId] = NativeStakingLock(address(0), 0); - // TODO: should "jobId => operator" data be pulled from JobManager to update operatorLockedAmounts? - - // TODO: totalLockedAmounts - - // TODO: distribute reward - - - // TODO: emit event - } - /*======================================== Overrides ========================================*/ function supportsInterface(bytes4 interfaceId) From 082b2e32a53cc988f161904d7c369c407333f894 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 00:30:41 +0900 Subject: [PATCH 104/158] implement completeSubmission logic --- .../interfaces/staking/ISymbioticStaking.sol | 2 +- .../staking/l2_contracts/StakingManager.sol | 8 +- .../staking/l2_contracts/SymbioticStaking.sol | 82 ++++++++++++------- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index c69cee7..f1aa5d0 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -30,8 +30,8 @@ interface ISymbioticStaking is IStakingPool { struct ConfirmedTimestamp { uint256 captureTimestamp; - uint256 rewardShare; // TODO address transmitter; + uint256 transmitterComissionRate; } // event OperatorSnapshotSubmitted diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 39d8a15..ef573d0 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -24,14 +24,12 @@ contract StakingManager is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - address public jobManager; - EnumerableSet.AddressSet private stakingPoolSet; - mapping(address pool => uint256 weight) private stakingPoolWeight; - mapping(address pool => PoolConfig config) private poolConfig; + address public jobManager; - uint256 stakeDataTransmitterShare; + mapping(address pool => PoolConfig config) private poolConfig; + mapping(address pool => uint256 weight) private stakingPoolWeight; struct PoolConfig { uint256 weight; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 022445c..763f4b9 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -11,8 +11,6 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; -// TODO: vault => token info should be updated by the admin - contract SymbioticStaking is ContextUpgradeable, ERC165Upgradeable, @@ -30,7 +28,7 @@ contract SymbioticStaking is } uint256 submissionCooldown; // 18 decimal (in seconds) - uint256 baseTransmitterComission; // 18 decimal (in percentage) + uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) /* Job Status */ bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; @@ -41,6 +39,8 @@ contract SymbioticStaking is bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); EnumerableSet.AddressSet tokenSet; + + address public stakingManager; /* Config */ mapping(address token => uint256 amount) public amountToLock; @@ -63,6 +63,23 @@ contract SymbioticStaking is mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; + modifier onlyStakingManager() { + require(msg.sender == stakingManager, "Only StakingManager"); + _; + } + + function initialize(address _admin, address _stakingManager) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + __ReentrancyGuard_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + stakingManager = _stakingManager; + } + /*======================================== L1 to L2 Transmission ========================================*/ function submitVaultSnapshot( @@ -138,6 +155,16 @@ contract SymbioticStaking is // TODO: emit event } + // TODO: check if delegatedStake also gets unlocked + function unlockStake(uint256 _jobId) external { + // TODO: consider the case when new pool is added during job + + // TODO: only staking manager + lockInfo[_jobId].amount = 0; + + // TODO: emit event + } + function _selectLockToken() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); @@ -149,16 +176,6 @@ contract SymbioticStaking is return tokenSet.at(idx); } - // TODO: check if delegatedStake also gets unlocked - function unlockStake(uint256 _jobId) external { - // TODO: consider the case when new pool is added during job - - // TODO: only staking manager - lockInfo[_jobId].amount = 0; - - // TODO: emit event - } - function getOperatorStake(address _operator, address _token) public view returns (uint256) { return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; } @@ -241,19 +258,16 @@ contract SymbioticStaking is } function _completeSubmission(uint256 _captureTimestamp) internal { - // TODO: calc `transmitterComission` based on last submission - ConfirmedTimestamp memory confirmedTimestamp = ConfirmedTimestamp(_captureTimestamp, block.timestamp, msg.sender); - confirmedTimestamps.push(confirmedTimestamp); + uint256 transmitterComission = _calcTransmitterComissionRate(lastConfirmedTimestamp()); - // TODO: calculate rewards for the transmitter based on transmitterComission - - // TODO: Data transmitter should get transmitterComission% of the rewards - - + ConfirmedTimestamp memory confirmedTimestamp = ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); + confirmedTimestamps.push(confirmedTimestamp); - // TODO: "transmitterComission" should reflect incentivization mechanism based on "captureTimestamp - (lastCaptureTimestamp + submissionCooldown)" + // TODO: emit event + } - // TODO: emit SubmissionCompleted + function _calcTransmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { + // TODO: implement logic } /*======================================== Getters ========================================*/ @@ -263,24 +277,34 @@ contract SymbioticStaking is } function isSupportedToken(address _token) public view returns (bool) { - // TODO + return tokenSet.contains(_token); } - function isSupportedVault(address _vault) public view returns (bool) { - // TODO + /*======================================== Admin ========================================*/ + function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { + stakingManager = _stakingManager; + + // TODO: emit event } - /*======================================== Admin ========================================*/ function setSupportedToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { - // TODO + if (_isSupported) { + require(tokenSet.add(_token), "Token already exists"); + } else { + require(tokenSet.remove(_token), "Token does not exist"); + } } function setSubmissionCooldown(uint256 _submissionCooldown) external onlyRole(DEFAULT_ADMIN_ROLE) { submissionCooldown = _submissionCooldown; + + // TODO: emit event } function setBaseTransmitterComission(uint256 _baseTransmitterComission) external onlyRole(DEFAULT_ADMIN_ROLE) { - baseTransmitterComission = _baseTransmitterComission; + baseTransmitterComissionRate = _baseTransmitterComission; + + // TODO: emit event } /*======================================== Overrides ========================================*/ From 4d0357f7ec7aa5d1c83ca22b7a2d1e4d032d65f8 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 00:41:54 +0900 Subject: [PATCH 105/158] fix setShare logic --- contracts/staking/l2_contracts/StakingManager.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index ef573d0..d5bdde4 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -114,15 +114,13 @@ contract StakingManager is uint256 sum = 0; for (uint256 i = 0; i < _shares.length; i++) { + poolConfig[_pools[i]].weight = _shares[i]; + sum += _shares[i]; } - // as the weight is in percentage, the sum of the shares should be 1e18 + // as the weight is in percentage, the sum of the shares should be 1e18 (100%) require(sum == 1e18, "Invalid Shares"); - - for (uint256 i = 0; i < _pools.length; i++) { - poolConfig[_pools[i]].weight = _shares[i]; - } } /*======================================== Override ========================================*/ From 4412bf632374ba8cc89e09fc60372e2a41dcb887 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 00:57:14 +0900 Subject: [PATCH 106/158] update unlockStake logic --- contracts/staking/l2_contracts/NativeStaking.sol | 3 +++ contracts/staking/l2_contracts/SymbioticStaking.sol | 9 +++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 3b19d4e..66d1fab 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -166,6 +166,9 @@ contract NativeStaking is function unlockStake(uint256 _jobId) external onlyStakingManager { NativeStakingLock memory lock = jobLockedAmounts[_jobId]; + + if(lock.amount == 0) return; + operatorLockedAmounts[lock.operator][lock.token] -= lock.amount; delete jobLockedAmounts[_jobId]; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 763f4b9..d398918 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -143,7 +143,7 @@ contract SymbioticStaking is /*======================================== Job Creation ========================================*/ // TODO: check if delegatedStake also gets locked - function lockStake(uint256 _jobId, address _operator) external { + function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); uint256 stakedAmount = getOperatorStake(_operator, _token); require(stakedAmount >= amountToLock[_token], "Insufficient stake amount"); @@ -156,11 +156,8 @@ contract SymbioticStaking is } // TODO: check if delegatedStake also gets unlocked - function unlockStake(uint256 _jobId) external { - // TODO: consider the case when new pool is added during job - - // TODO: only staking manager - lockInfo[_jobId].amount = 0; + function unlockStake(uint256 _jobId) external onlyStakingManager { + delete lockInfo[_jobId]; // TODO: emit event } From 42aba9f3384afd436e86517a58745e7eaf845707 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 21:12:24 +0900 Subject: [PATCH 107/158] implement slashing logic --- contracts/interfaces/staking/IJobManager.sol | 3 + .../interfaces/staking/IStakingManager.sol | 6 +- contracts/interfaces/staking/IStakingPool.sol | 6 +- .../interfaces/staking/ISymbioticStaking.sol | 10 ---- contracts/interfaces/staking/lib/Struct.sol | 21 +++++++ contracts/staking/l2_contracts/JobManager.sol | 45 ++++++++------- .../staking/l2_contracts/NativeStaking.sol | 29 ++++++++-- .../staking/l2_contracts/StakingManager.sol | 25 +++++++- .../staking/l2_contracts/SymbioticStaking.sol | 57 +++++++++++-------- 9 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 contracts/interfaces/staking/lib/Struct.sol diff --git a/contracts/interfaces/staking/IJobManager.sol b/contracts/interfaces/staking/IJobManager.sol index 963f683..b9ba71f 100644 --- a/contracts/interfaces/staking/IJobManager.sol +++ b/contracts/interfaces/staking/IJobManager.sol @@ -3,5 +3,8 @@ pragma solidity ^0.8.26; interface IJobManager { function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external; + function submitProof(uint256 jobId, bytes calldata proof) external; + + function refundFee(uint256 jobId) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 33ac19b..15be550 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -1,9 +1,13 @@ // SPDX-License-Identifier: MIT +import {Struct} from "./lib/Struct.sol"; + pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; - function onJobCompletion(uint256 jobId) external; + function onJobCompletion(uint256 jobId, address operator) external; + + function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index 5e3628b..c92a0d0 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +import {Struct} from "./lib/Struct.sol"; + interface IStakingPool { function isSupportedToken(address _token) external view returns (bool); @@ -8,14 +10,16 @@ interface IStakingPool { function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only - function unlockStake(uint256 _jobId) external; // Staking Manager only + function unlockStake(uint256 _jobId, address _operator) external; // Staking Manager only + function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only struct PoolLockInfo { address token; uint256 amount; address transmitter; } + // struct NativeStakingLock { // address token; // uint256 amount; diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index f1aa5d0..892da3d 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -18,16 +18,6 @@ interface ISymbioticStaking is IStakingPool { uint256 stake; } - struct SlashResultData { - uint256 jobId; - SlashResult slashResult; - } - - struct SlashResult { - uint256 slashAmount; - address rewardAddress; // address that transmitted slash reqeust to L1 Vault - } - struct ConfirmedTimestamp { uint256 captureTimestamp; address transmitter; diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/interfaces/staking/lib/Struct.sol new file mode 100644 index 0000000..5f32edb --- /dev/null +++ b/contracts/interfaces/staking/lib/Struct.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +library Struct { + struct PoolLockInfo { + address token; + uint256 amount; + address transmitter; + } + + struct NativeStakingLock { + address token; + uint256 amount; + } + + struct JobSlashed { + uint256 jobId; + address operator; + address rewardAddress; + } +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 72ca50d..0936322 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -17,7 +17,7 @@ import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; JobManager contract is responsible for creating and managing jobs. Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. */ -contract JobManager is +contract JobManager is ContextUpgradeable, ERC165Upgradeable, AccessControlUpgradeable, @@ -42,7 +42,10 @@ contract JobManager is uint256 public jobDuration; uint256 public totalFeeStored; // TODO: check if needed - function initialize(address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) public initializer { + function initialize(address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) + public + initializer + { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -56,9 +59,12 @@ contract JobManager is } // TODO: check paramter for job details - function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external nonReentrant { + function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) + external + nonReentrant + { IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); - + // stakeToken and lockAmount will be decided in each pool jobs[_jobId] = JobInfo({ requester: _requester, @@ -66,7 +72,7 @@ contract JobManager is feePaid: _feeAmount, deadline: block.timestamp + jobDuration }); - + IStakingManager(stakingManager).onJobCreation(_jobId, _operator); totalFeeStored += _feeAmount; @@ -82,7 +88,7 @@ contract JobManager is _verifyProof(_jobId, _proof); - IStakingManager(stakingManager).onJobCompletion(_jobId); // unlock stake + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator); // unlock stake } /** @@ -95,28 +101,27 @@ contract JobManager is uint256 len = _jobIds.length; for (uint256 idx = 0; idx < len; idx++) { - uint256 jobId = _jobIds[idx]; - require(block.timestamp <= jobs[jobId].deadline, "Job Expired"); - - _verifyProof(jobId, _proofs[idx]); + uint256 _jobId = _jobIds[idx]; + require(block.timestamp <= jobs[_jobId].deadline, "Job Expired"); + + _verifyProof(_jobId, _proofs[idx]); // TODO: let onJobCompletion also accept array of jobIds - IStakingManager(stakingManager).onJobCompletion(jobId); // unlock stake + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator); // unlock stake } - } function refundFee(uint256 _jobId) external nonReentrant { require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); - require(jobs[_jobId].requester == msg.sender, "Not Requester"); - // TODO: refund fee - jobs[_jobId].feePaid = 0; - totalFeeStored -= jobs[_jobId].feePaid; + if (jobs[_jobId].feePaid > 0) { + jobs[_jobId].feePaid = 0; + totalFeeStored -= jobs[_jobId].feePaid; - IERC20(feeToken).safeTransfer(jobs[_jobId].requester, jobs[_jobId].feePaid); - - // TODO: emit event + IERC20(feeToken).safeTransfer(jobs[_jobId].requester, jobs[_jobId].feePaid); + + // TODO: emit event + } } function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { @@ -152,4 +157,4 @@ contract JobManager is } function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} -} \ No newline at end of file +} diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 66d1fab..6a292dc 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -11,9 +11,12 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; import {INativeStakingReward} from "../../interfaces/staking/INativeStakingReward.sol"; +import {Struct} from "../../interfaces/staking/lib/Struct.sol"; + contract NativeStaking is ContextUpgradeable, ERC165Upgradeable, @@ -26,7 +29,6 @@ contract NativeStaking is using SafeERC20 for IERC20; struct NativeStakingLock { - address operator; address token; uint256 amount; } @@ -46,7 +48,7 @@ contract NativeStaking is mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; /* Locked Stakes */ - mapping(uint256 jobId => NativeStakingLock) public jobLockedAmounts; + mapping(uint256 jobId => NativeStakingLock lock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; modifier onlySupportedToken(address _token) { @@ -158,18 +160,18 @@ contract NativeStaking is require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake - jobLockedAmounts[_jobId] = NativeStakingLock(_operator, _token, _amountToLock); + jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; // TODO: emit event } - function unlockStake(uint256 _jobId) external onlyStakingManager { + function unlockStake(uint256 _jobId, address _operator) external onlyStakingManager { NativeStakingLock memory lock = jobLockedAmounts[_jobId]; if(lock.amount == 0) return; - operatorLockedAmounts[lock.operator][lock.token] -= lock.amount; + operatorLockedAmounts[_operator][lock.token] -= lock.amount; delete jobLockedAmounts[_jobId]; // TODO: distribute reward @@ -177,6 +179,23 @@ contract NativeStaking is // TODO: emit event } + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { + uint256 len = _slashedJobs.length; + for (uint256 i = 0; i < len; i++) { + NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; + + uint256 lockedAmount = lock.amount; + if(lockedAmount == 0) continue; // if already slashed + + operatorLockedAmounts[_slashedJobs[i].operator][lock.token] -= lockedAmount; + delete jobLockedAmounts[_slashedJobs[i].jobId]; + + IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); + + // TODO: emit event + } + } + function _selectTokenToLock() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index d5bdde4..4af146f 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -12,6 +12,9 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; + +import {Struct} from "../../interfaces/staking/lib/Struct.sol"; contract StakingManager is ContextUpgradeable, @@ -68,17 +71,33 @@ contract StakingManager is } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId) external onlyJobManager { + function onJobCompletion(uint256 _jobId, address _operator) external onlyJobManager { uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - IStakingPool(pool).unlockStake(_jobId); + IStakingPool(pool).unlockStake(_jobId, _operator); } - // TODO: emit event } + function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { + // msg.sender will most likely be SymbioticStaking contract + require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); + + for(uint256 i = 0; i < _jobsSlashed.length; i++) { + IJobManager(jobManager).refundFee(_jobsSlashed[i].jobId); + } + + uint256 len = stakingPoolSet.length(); + for (uint256 i = 0; i < len; i++) { + address pool = stakingPoolSet.at(i); + if(pool == msg.sender) continue; + + IStakingPool(pool).slash(_jobsSlashed); + } + } + /*======================================== Getters ========================================*/ function isEnabledPool(address _pool) public view returns (bool) { return poolConfig[_pool].enabled; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index d398918..8bc4705 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -9,8 +9,11 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {Struct} from "../../interfaces/staking/lib/Struct.sol"; + contract SymbioticStaking is ContextUpgradeable, ERC165Upgradeable, @@ -52,14 +55,13 @@ contract SymbioticStaking is mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; // staked amount for each vault mapping(uint256 captureTimestamp => mapping(address vault => mapping(address token => uint256 stakeAmount))) vaultStakedAmounts; - // slash result for each job - mapping(uint256 captureTimestamp => mapping(uint256 jobId => SlashResult slashResult)) slashResults; ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received /* Staking */ mapping(uint256 jobId => SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake + mapping(address operator => mapping(address token => uint256 locked)) public operatorLockedAmounts; mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; @@ -123,8 +125,10 @@ contract SymbioticStaking is _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultData, _signature); - SlashResultData[] memory _SlashResultDatas = abi.decode(_SlashResultData, (SlashResultData[])); - _updateSlashResultDataInfo(_captureTimestamp, _SlashResultDatas); + //? is there any need to store the slash result data? + //? is there any need to even lock stake here? - anyway it'll just be slashed in L1 + Struct.JobSlashed[] memory _jobSlashed = abi.decode(_SlashResultData, (Struct.JobSlashed[])); + // _updateSlashResultDataInfo(_captureTimestamp, _jobSlashed); _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); @@ -137,31 +141,48 @@ contract SymbioticStaking is if (_isCompleteStatus(_captureTimestamp)) { _completeSubmission(_captureTimestamp); } + + IStakingManager(stakingManager).onSlashResult(_jobSlashed); // TODO: unlock the selfStake and reward it to the transmitter } - /*======================================== Job Creation ========================================*/ - // TODO: check if delegatedStake also gets locked + /*======================================== Job ========================================*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); - uint256 stakedAmount = getOperatorStake(_operator, _token); - require(stakedAmount >= amountToLock[_token], "Insufficient stake amount"); + require(getOperatorActiveStake(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; lockInfo[_jobId] = SymbioticStakingLock(_token, amountToLock[_token], transmitter); + operatorLockedAmounts[_operator][_token] += amountToLock[_token]; // TODO: emit event } - // TODO: check if delegatedStake also gets unlocked - function unlockStake(uint256 _jobId) external onlyStakingManager { + function unlockStake(uint256 _jobId, address _operatorId) external onlyStakingManager { + // TODO: reward the transmitter + delete lockInfo[_jobId]; // TODO: emit event } + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { + uint256 len = _slashedJobs.length; + for (uint256 i = 0; i < len; i++) { + SymbioticStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; + + uint256 lockedAmount = lock.amount; + if(lockedAmount == 0) continue; + + operatorLockedAmounts[_slashedJobs[i].operator][lock.token] -= lockedAmount; + delete lockInfo[_slashedJobs[i].jobId]; + + // TODO: emit events? + } + } + function _selectLockToken() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); @@ -173,8 +194,8 @@ contract SymbioticStaking is return tokenSet.at(idx); } - function getOperatorStake(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; + function getOperatorActiveStake(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token] - operatorLockedAmounts[_operator][_token]; } /*======================================== Helpers ========================================*/ @@ -242,18 +263,6 @@ contract SymbioticStaking is } } - function _updateSlashResultDataInfo(uint256 _captureTimestamp, SlashResultData[] memory _SlashResultDatas) - internal - { - for (uint256 i = 0; i < _SlashResultDatas.length; i++) { - SlashResultData memory _slashResultData = _SlashResultDatas[i]; - - slashResults[_slashResultData.jobId][_captureTimestamp] = _slashResultData.slashResult; - - // TODO: emit event for each update? - } - } - function _completeSubmission(uint256 _captureTimestamp) internal { uint256 transmitterComission = _calcTransmitterComissionRate(lastConfirmedTimestamp()); From 4bd8823bc0cb503c9ea2186d4104560cdf082124 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 27 Sep 2024 21:16:04 +0900 Subject: [PATCH 108/158] move struct to Struct.sol --- contracts/interfaces/staking/IStakingPool.sol | 11 ---------- .../interfaces/staking/ISymbioticStaking.sol | 18 +---------------- contracts/interfaces/staking/lib/Struct.sol | 18 +++++++++++++++++ .../staking/l2_contracts/SymbioticStaking.sol | 20 +++++++++---------- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index c92a0d0..0a4dafd 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -13,15 +13,4 @@ interface IStakingPool { function unlockStake(uint256 _jobId, address _operator) external; // Staking Manager only function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only - struct PoolLockInfo { - address token; - uint256 amount; - address transmitter; - } - - - // struct NativeStakingLock { - // address token; - // uint256 amount; - // } } \ No newline at end of file diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index 892da3d..cee7c87 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -6,23 +6,7 @@ import {IStakingPool} from "./IStakingPool.sol"; interface ISymbioticStaking is IStakingPool { // function stakeOf(address _operator, address _token) external view returns (uint256); - struct SnapshotTxCountInfo { - uint256 idxToSubmit; // idx of pratial snapshot tx to submit - uint256 numOfTxs; // total number of txs for the snapshot - } - - struct VaultSnapshot { - address operator; - address vault; - address token; - uint256 stake; - } - - struct ConfirmedTimestamp { - uint256 captureTimestamp; - address transmitter; - uint256 transmitterComissionRate; - } + // event OperatorSnapshotSubmitted diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/interfaces/staking/lib/Struct.sol index 5f32edb..3c28d02 100644 --- a/contracts/interfaces/staking/lib/Struct.sol +++ b/contracts/interfaces/staking/lib/Struct.sol @@ -18,4 +18,22 @@ library Struct { address operator; address rewardAddress; } + + struct SnapshotTxCountInfo { + uint256 idxToSubmit; // idx of pratial snapshot tx to submit + uint256 numOfTxs; // total number of txs for the snapshot + } + + struct VaultSnapshot { + address operator; + address vault; + address token; + uint256 stake; + } + + struct ConfirmedTimestamp { + uint256 captureTimestamp; + address transmitter; + uint256 transmitterComissionRate; + } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 8bc4705..5fc174d 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -49,14 +49,14 @@ contract SymbioticStaking is mapping(address token => uint256 amount) public amountToLock; /* Symbiotic Snapshot */ - mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received + mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received // staked amount for each operator mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; // staked amount for each vault mapping(uint256 captureTimestamp => mapping(address vault => mapping(address token => uint256 stakeAmount))) vaultStakedAmounts; - ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received + Struct.ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received /* Staking */ @@ -98,12 +98,12 @@ contract SymbioticStaking is _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); // main update logic - VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (VaultSnapshot[])); + Struct.VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (Struct.VaultSnapshot[])); _updateSnapshotInfo(_captureTimestamp, _vaultSnapshots); _updateTxCountInfo(_numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT_MASK; @@ -132,7 +132,7 @@ contract SymbioticStaking is _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT; @@ -206,7 +206,7 @@ contract SymbioticStaking is require(block.timestamp >= (lastConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); - SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; + Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; require(_index == snapshot.idxToSubmit, "Invalid index"); require(snapshot.idxToSubmit < snapshot.numOfTxs, "Snapshot fully submitted already"); require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); @@ -228,7 +228,7 @@ contract SymbioticStaking is } function _updateTxCountInfo(uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { - SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; // increase count by 1 txCountInfo[_captureTimestamp][msg.sender][_type].idxToSubmit += 1; @@ -249,9 +249,9 @@ contract SymbioticStaking is } - function _updateSnapshotInfo(uint256 _captureTimestamp, VaultSnapshot[] memory _vaultSnapshots) internal { + function _updateSnapshotInfo(uint256 _captureTimestamp, Struct.VaultSnapshot[] memory _vaultSnapshots) internal { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { - VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; + Struct.VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; // update vault staked amount vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.token] = _vaultSnapshot.stake; @@ -266,7 +266,7 @@ contract SymbioticStaking is function _completeSubmission(uint256 _captureTimestamp) internal { uint256 transmitterComission = _calcTransmitterComissionRate(lastConfirmedTimestamp()); - ConfirmedTimestamp memory confirmedTimestamp = ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); + Struct.ConfirmedTimestamp memory confirmedTimestamp = Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); confirmedTimestamps.push(confirmedTimestamp); // TODO: emit event From c716bd0b7da653da753f85b0bccf101665090a1b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 28 Sep 2024 17:59:28 +0900 Subject: [PATCH 109/158] remove lock/unlock from SymbioticStakingReward --- .../l2_contracts/SymbioticStakingReward.sol | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index e087448..b449789 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -146,27 +146,6 @@ contract SymbioticStakingReward is } - function lockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { - require(amount <= totalStakeAmountsActive(_stakeToken), "insufficient stake amount"); - - lockedStakeAmounts[_latestConfirmedTimestamp()][_stakeToken] += amount; - - _updateRewardPerTokens(_stakeToken); - - // TODO: emit event - } - - function unlockStake(address _stakeToken, uint256 amount) external onlySymbioticStaking { - uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); - require(amount <= lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken], "insufficient locked stake amount"); - - lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken] -= amount; - - _updateRewardPerTokens(_stakeToken); - - // TODO: emit event - } - /// @notice rewardToken amount per stakeToken function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { uint256 totalStakeAmount = totalStakeAmountsActive(_stakeToken); From 6db8edea66fa6a7d61f7eae8462ec10bcb29bd5d Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 30 Sep 2024 13:26:01 +0900 Subject: [PATCH 110/158] complete reward distribution logic --- .../interfaces/staking/IRewardDistributor.sol | 7 +++ .../interfaces/staking/IStakingManager.sol | 2 +- contracts/interfaces/staking/IStakingPool.sol | 6 +- contracts/interfaces/staking/lib/Struct.sol | 23 ++++++++ contracts/staking/l2_contracts/JobManager.sol | 22 +++---- .../staking/l2_contracts/NativeStaking.sol | 50 ++++++++++------ .../l2_contracts/NativeStakingReward.sol | 51 +++++++--------- .../staking/l2_contracts/StakingManager.sol | 21 ++++--- .../staking/l2_contracts/SymbioticStaking.sol | 59 ++++++++++++------- .../l2_contracts/SymbioticStakingReward.sol | 4 +- 10 files changed, 152 insertions(+), 93 deletions(-) create mode 100644 contracts/interfaces/staking/IRewardDistributor.sol diff --git a/contracts/interfaces/staking/IRewardDistributor.sol b/contracts/interfaces/staking/IRewardDistributor.sol new file mode 100644 index 0000000..7b1b9bd --- /dev/null +++ b/contracts/interfaces/staking/IRewardDistributor.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +interface IRewardDistributor { + function addReward(address _stakeToken, address operator, address _rewardToken, uint256 _amount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 15be550..2260159 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; - function onJobCompletion(uint256 jobId, address operator) external; + function onJobCompletion(uint256 jobId, address operator, uint256 feePaid) external; function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index 0a4dafd..f1e022b 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -10,7 +10,11 @@ interface IStakingPool { function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only - function unlockStake(uint256 _jobId, address _operator) external; // Staking Manager only + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external; // Staking Manager only function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only + + function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256); + + function rewardDistributor() external view returns (address); } \ No newline at end of file diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/interfaces/staking/lib/Struct.sol index 3c28d02..55df3c4 100644 --- a/contracts/interfaces/staking/lib/Struct.sol +++ b/contracts/interfaces/staking/lib/Struct.sol @@ -2,12 +2,23 @@ pragma solidity ^0.8.26; library Struct { + + /* Job Manager */ + struct JobInfo { + address requester; + address operator; + uint256 feePaid; + uint256 deadline; + } + + /* Staking Pool */ struct PoolLockInfo { address token; uint256 amount; address transmitter; } + /* NativeStaking */ struct NativeStakingLock { address token; uint256 amount; @@ -36,4 +47,16 @@ library Struct { address transmitter; uint256 transmitterComissionRate; } + + struct SymbioticStakingLock { + address stakeToken; + uint256 amount; + // transmitter who submitted with confirmedTimestamp used when job is created + address transmitter; + } + struct PoolConfig { + uint256 weight; + bool enabled; + } + } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 0936322..2f7eb55 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -12,6 +12,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; +import {Struct} from "../../interfaces/staking/lib/Struct.sol"; /* JobManager contract is responsible for creating and managing jobs. @@ -27,14 +28,7 @@ contract JobManager is { using SafeERC20 for IERC20; - struct JobInfo { - address requester; - address operator; - uint256 feePaid; - uint256 deadline; - } - - mapping(uint256 jobId => JobInfo jobInfo) public jobs; + mapping(uint256 jobId => Struct.JobInfo jobInfo) public jobs; address public stakingManager; address public feeToken; @@ -66,7 +60,7 @@ contract JobManager is IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); // stakeToken and lockAmount will be decided in each pool - jobs[_jobId] = JobInfo({ + jobs[_jobId] = Struct.JobInfo({ requester: _requester, operator: _operator, feePaid: _feeAmount, @@ -88,7 +82,9 @@ contract JobManager is _verifyProof(_jobId, _proof); - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator); // unlock stake + // send fee and unlock stake + IERC20(feeToken).safeTransfer(stakingManager, jobs[_jobId].feePaid); // TODO: make RewardDistributor pull fee from JobManager + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, jobs[_jobId].feePaid); } /** @@ -107,14 +103,14 @@ contract JobManager is _verifyProof(_jobId, _proofs[idx]); // TODO: let onJobCompletion also accept array of jobIds - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator); // unlock stake + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, jobs[_jobId].feePaid); // unlock stake } } function refundFee(uint256 _jobId) external nonReentrant { - require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); - if (jobs[_jobId].feePaid > 0) { + require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); + jobs[_jobId].feePaid = 0; totalFeeStored -= jobs[_jobId].feePaid; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 6a292dc..ac627ea 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -14,6 +14,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; import {INativeStakingReward} from "../../interfaces/staking/INativeStakingReward.sol"; +import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; import {Struct} from "../../interfaces/staking/lib/Struct.sol"; @@ -28,15 +29,13 @@ contract NativeStaking is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - struct NativeStakingLock { - address token; - uint256 amount; - } + EnumerableSet.AddressSet private tokenSet; - address public nativeStakingReward; + address public rewardDistributor; address public stakingManager; + address public feeRewardToken; /* Config */ mapping(address token => uint256 lockAmount) public amountToLock; @@ -48,7 +47,7 @@ contract NativeStaking is mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; /* Locked Stakes */ - mapping(uint256 jobId => NativeStakingLock lock) public jobLockedAmounts; + mapping(uint256 jobId => Struct.NativeStakingLock lock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; modifier onlySupportedToken(address _token) { @@ -86,7 +85,7 @@ contract NativeStaking is // NativeStakingReward contract will read staking amount info from this contract // and update reward related states - INativeStakingReward(nativeStakingReward).update(msg.sender, _token, _operator); + INativeStakingReward(rewardDistributor).update(msg.sender, _token, _operator); emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -100,7 +99,7 @@ contract NativeStaking is IERC20(_token).safeTransfer(msg.sender, _amount); - INativeStakingReward(nativeStakingReward).update(msg.sender, _token, _operator); + INativeStakingReward(rewardDistributor).update(msg.sender, _token, _operator); emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); } @@ -112,6 +111,10 @@ contract NativeStaking is /*======================================== Getters ========================================*/ + function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[_operator][_token]; + } + function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { return operatorStakedAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; } @@ -136,7 +139,7 @@ contract NativeStaking is } function setNativeStakingReward(address _nativeStakingReward) external onlyRole(DEFAULT_ADMIN_ROLE) { - nativeStakingReward = _nativeStakingReward; + rewardDistributor = _nativeStakingReward; // TODO: emit event } @@ -160,19 +163,21 @@ contract NativeStaking is require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake - jobLockedAmounts[_jobId] = NativeStakingLock(_token, _amountToLock); + jobLockedAmounts[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; // TODO: emit event } - function unlockStake(uint256 _jobId, address _operator) external onlyStakingManager { - NativeStakingLock memory lock = jobLockedAmounts[_jobId]; + /// @notice unlock stake and distribute reward + /// @dev called by StakingManager when job is completed + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external onlyStakingManager { + Struct.NativeStakingLock memory lock = jobLockedAmounts[_jobId]; if(lock.amount == 0) return; - operatorLockedAmounts[_operator][lock.token] -= lock.amount; - delete jobLockedAmounts[_jobId]; + _unlockStake(_jobId, _operator, lock.token, lock.amount); + _distributeReward(lock.token, _operator, feeRewardToken, _feeRewardAmount); // TODO: distribute reward @@ -182,20 +187,29 @@ contract NativeStaking is function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { - NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; + Struct.NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; uint256 lockedAmount = lock.amount; if(lockedAmount == 0) continue; // if already slashed - operatorLockedAmounts[_slashedJobs[i].operator][lock.token] -= lockedAmount; - delete jobLockedAmounts[_slashedJobs[i].jobId]; - + _unlockStake(_slashedJobs[i].jobId, _slashedJobs[i].operator, lock.token, lockedAmount); IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); // TODO: emit event } } + function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { + operatorLockedAmounts[_operator][_stakeToken] -= _amount; + delete jobLockedAmounts[_jobId]; + } + + function _distributeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { + IERC20(_rewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addReward(_stakeToken, _operator, _rewardToken, _amount); + } + + function _selectTokenToLock() internal view returns(address) { require(tokenSet.length() > 0, "No supported token"); diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 41376e3..52b3161 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -37,17 +37,12 @@ contract NativeStakingReward is mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) rewardPerTokens; mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) userRewardPerTokenPaid; - mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount)))) claimableRewards; + mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount)))) rewardAccrued; - // TODO: (function) stake - - // TODO: (function) unstake - - // TODO: (function) claim reward - - // TODO: (function) update - - // TODO: (function) addReward + modifier onlyNativeStaking() { + require(msg.sender == nativeStaking, "Only NativeStaking"); + _; + } //-------------------------------- Init start --------------------------------// @@ -60,7 +55,8 @@ contract NativeStakingReward is __ReentrancyGuard_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); - _setNativeStaking(_nativeStaking); + + nativeStaking = _nativeStaking; } //-------------------------------- Init end --------------------------------// @@ -70,18 +66,16 @@ contract NativeStakingReward is } - function addReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) public { - // TODO: only native staking - + function addReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) public onlyNativeStaking { rewards[_stakeToken][_operator][_rewardToken] += _amount; _update(address(0), _stakeToken, _operator, _rewardToken); } - /// @notice pulls stake amount info from NativeStaking and updates - /// @notice stake amount will be tracked in NativeStaking - function update(address account, address _stakeToken, address _operator) public { - // TODO: only native staking + function updateFeeReward(address account, address _stakeToken, address _operator) public onlyNativeStaking { _update(account, _stakeToken, _operator, feeRewardToken); + } + + function updateInflationReward(address account, address _stakeToken, address _operator) public onlyNativeStaking { _update(account, _stakeToken, _operator, inflationRewardToken); } @@ -89,8 +83,10 @@ contract NativeStakingReward is uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _operator, _rewardToken); rewardPerTokens[_stakeToken][_operator][_rewardToken] = currentRewardPerToken; - claimableRewards[account][_stakeToken][_operator][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); - userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + if(account != address(0)) { + rewardAccrued[account][_stakeToken][_operator][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); + userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + } } function _pendingReward(address account, address _stakeToken, address operator, address _rewardToken) internal view returns (uint256) { @@ -102,17 +98,17 @@ contract NativeStakingReward is } function _rewardPerToken(address _stakeToken, address _operator, address _rewardToken) internal view returns (uint256) { - uint256 totalStakeAmount = _getTotalStakeAmountActive(_stakeToken, _operator); + uint256 operatorStakeAmount = _getOperatorStakeAmount(_stakeToken, _operator); uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; // TODO: make sure decimal is 18 - return totalStakeAmount == 0 + return operatorStakeAmount == 0 ? rewardPerTokens[_stakeToken][_operator][_rewardToken] - : rewardPerTokens[_stakeToken][_operator][_rewardToken] + totalRewardAmount.mulDiv(1e18, totalStakeAmount); + : rewardPerTokens[_stakeToken][_operator][_rewardToken] + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); } - function _getTotalStakeAmountActive(address token, address operator) internal view returns (uint256) { - // return INativeStaking(nativeStaking).getTotalStakeAmountActive(token, operator); + function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { + return INativeStaking(nativeStaking).getOperatorStakeAmount(_operator, _stakeToken); } function _getUserStakeAmount(address account, address token, address operator) internal view returns (uint256) { @@ -131,11 +127,8 @@ contract NativeStakingReward is //-------------------------------- Overrides start --------------------------------// function setNativeStaking(address _nativeStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { - _setNativeStaking(_nativeStaking); - } - - function _setNativeStaking(address _nativeStaking) internal { nativeStaking = _nativeStaking; + // TODO: emit event } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 4af146f..f04b396 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -13,8 +13,10 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract StakingManager is ContextUpgradeable, @@ -30,15 +32,12 @@ contract StakingManager is EnumerableSet.AddressSet private stakingPoolSet; address public jobManager; + address public feeToken; + address public inflationRewardToken; - mapping(address pool => PoolConfig config) private poolConfig; + mapping(address pool => Struct.PoolConfig config) private poolConfig; mapping(address pool => uint256 weight) private stakingPoolWeight; - struct PoolConfig { - uint256 weight; - bool enabled; - } - modifier onlyJobManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); _; @@ -71,16 +70,22 @@ contract StakingManager is } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId, address _operator) external onlyJobManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feePaid) external onlyJobManager { uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - IStakingPool(pool).unlockStake(_jobId, _operator); + uint256 feeRewardAmount = _calcFeeRewardAmount(pool, _feePaid); + IERC20(feeToken).safeTransfer(pool, feeRewardAmount); + IStakingPool(pool).unlockStake(_jobId, _operator, feeRewardAmount); } // TODO: emit event } + function _calcFeeRewardAmount(address _pool, uint256 _feePaid) internal view returns (uint256) { + return Math.mulDiv(_feePaid, poolConfig[_pool].weight, 1e18); + } + function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { // msg.sender will most likely be SymbioticStaking contract require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 5fc174d..8f9ef92 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -13,6 +13,10 @@ import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; contract SymbioticStaking is ContextUpgradeable, @@ -23,12 +27,7 @@ contract SymbioticStaking is ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; - struct SymbioticStakingLock { - address token; - uint256 amount; - // transmitter who submitted with confirmedTimestamp used when job is created - address transmitter; - } + using SafeERC20 for IERC20; uint256 submissionCooldown; // 18 decimal (in seconds) uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) @@ -43,7 +42,9 @@ contract SymbioticStaking is EnumerableSet.AddressSet tokenSet; + address public feeRewardToken; address public stakingManager; + address public rewardDistributor; /* Config */ mapping(address token => uint256 amount) public amountToLock; @@ -60,17 +61,17 @@ contract SymbioticStaking is /* Staking */ - mapping(uint256 jobId => SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake + mapping(uint256 jobId => Struct.SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake mapping(address operator => mapping(address token => uint256 locked)) public operatorLockedAmounts; - mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; + mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; // only one transmitter can submit the snapshot for the same capturetimestamp modifier onlyStakingManager() { require(msg.sender == stakingManager, "Only StakingManager"); _; } - function initialize(address _admin, address _stakingManager) public initializer { + function initialize(address _admin, address _stakingManager, address _rewardDistributor) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -80,6 +81,7 @@ contract SymbioticStaking is _grantRole(DEFAULT_ADMIN_ROLE, _admin); stakingManager = _stakingManager; + rewardDistributor = _rewardDistributor; } /*======================================== L1 to L2 Transmission ========================================*/ @@ -150,20 +152,31 @@ contract SymbioticStaking is /*======================================== Job ========================================*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); - require(getOperatorActiveStake(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); + require(getOperatorActiveStakeAmount(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); // Store transmitter address to reward when job is closed address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - lockInfo[_jobId] = SymbioticStakingLock(_token, amountToLock[_token], transmitter); + lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], transmitter); operatorLockedAmounts[_operator][_token] += amountToLock[_token]; // TODO: emit event } - function unlockStake(uint256 _jobId, address _operatorId) external onlyStakingManager { - // TODO: reward the transmitter + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external onlyStakingManager { + Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; + uint256 transmitterComissionRate = confirmedTimestamps[confirmedTimestamps.length - 1].transmitterComissionRate; + uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); + uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; + + // reward transmitter + IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); + + // distribute reward + IERC20(feeRewardToken).safeTransfer(rewardDistributor, feeRewardRemaining); + IRewardDistributor(rewardDistributor).addReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); delete lockInfo[_jobId]; + operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; // TODO: emit event } @@ -171,12 +184,12 @@ contract SymbioticStaking is function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { - SymbioticStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; + Struct.SymbioticStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; uint256 lockedAmount = lock.amount; if(lockedAmount == 0) continue; - operatorLockedAmounts[_slashedJobs[i].operator][lock.token] -= lockedAmount; + operatorLockedAmounts[_slashedJobs[i].operator][lock.stakeToken] -= lockedAmount; delete lockInfo[_slashedJobs[i].jobId]; // TODO: emit events? @@ -194,10 +207,6 @@ contract SymbioticStaking is return tokenSet.at(idx); } - function getOperatorActiveStake(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token] - operatorLockedAmounts[_operator][_token]; - } - /*======================================== Helpers ========================================*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { require(_numOfTxs > 0, "Invalid length"); @@ -264,7 +273,7 @@ contract SymbioticStaking is } function _completeSubmission(uint256 _captureTimestamp) internal { - uint256 transmitterComission = _calcTransmitterComissionRate(lastConfirmedTimestamp()); + uint256 transmitterComission = _transmitterComissionRate(lastConfirmedTimestamp()); Struct.ConfirmedTimestamp memory confirmedTimestamp = Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); confirmedTimestamps.push(confirmedTimestamp); @@ -272,7 +281,7 @@ contract SymbioticStaking is // TODO: emit event } - function _calcTransmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { + function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { // TODO: implement logic } @@ -282,6 +291,14 @@ contract SymbioticStaking is return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; } + function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; + } + + function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token] - operatorLockedAmounts[_operator][_token]; + } + function isSupportedToken(address _token) public view returns (bool) { return tokenSet.contains(_token); } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index b449789..0227110 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -134,9 +134,9 @@ contract SymbioticStakingReward is function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external onlySymbioticStaking { require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); - require(_amount > 0, "zero amount"); + // require(_amount > 0, "zero amount"); - IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); + // IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); // update rewardPerToken uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); From 66ca214ed801c6c7fc1828f7d732da8f307c79d2 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 30 Sep 2024 13:36:24 +0900 Subject: [PATCH 111/158] refactor code --- contracts/staking/l2_contracts/NativeStaking.sol | 2 -- contracts/staking/l2_contracts/StakingManager.sol | 6 +++++- contracts/staking/l2_contracts/SymbioticStaking.sol | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index ac627ea..1e3d292 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -179,8 +179,6 @@ contract NativeStaking is _unlockStake(_jobId, _operator, lock.token, lock.amount); _distributeReward(lock.token, _operator, feeRewardToken, _feeRewardAmount); - // TODO: distribute reward - // TODO: emit event } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index f04b396..39af33b 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -86,18 +86,22 @@ contract StakingManager is return Math.mulDiv(_feePaid, poolConfig[_pool].weight, 1e18); } + /// @notice called by SymbioticStaking contract when slash result is submitted function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { // msg.sender will most likely be SymbioticStaking contract require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); + // refund fee to the requester for(uint256 i = 0; i < _jobsSlashed.length; i++) { + // this can be done manually in the JobManager contract + // refunds nothing if already refunded IJobManager(jobManager).refundFee(_jobsSlashed[i].jobId); } uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - if(pool == msg.sender) continue; + if(pool == msg.sender) continue; // skip if called by SymbioticStaking IStakingPool(pool).slash(_jobsSlashed); } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 8f9ef92..d7bfbcc 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -168,12 +168,8 @@ contract SymbioticStaking is uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; - // reward transmitter - IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); - - // distribute reward - IERC20(feeRewardToken).safeTransfer(rewardDistributor, feeRewardRemaining); - IRewardDistributor(rewardDistributor).addReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); + IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); // reward transmitter + _distributeReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); delete lockInfo[_jobId]; operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; @@ -181,6 +177,11 @@ contract SymbioticStaking is // TODO: emit event } + function _distributeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { + IERC20(_rewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addReward(_stakeToken, _operator, _rewardToken, _amount); + } + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { From 2ca8ae25e636876d01f362a2ab1715bf5314c4a0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 30 Sep 2024 14:04:47 +0900 Subject: [PATCH 112/158] refactor code --- .../staking/l2_contracts/SymbioticStaking.sol | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index d7bfbcc..d7babf7 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -33,11 +33,11 @@ contract SymbioticStaking is uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) /* Job Status */ - bytes32 public constant VAULT_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 public constant STAKE_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000011; - bytes32 public constant VAULT_SNAPSHOT = keccak256("VAULT_SNAPSHOT"); + bytes32 public constant STAKE_SNAPSHOT = keccak256("STAKE_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); EnumerableSet.AddressSet tokenSet; @@ -95,7 +95,7 @@ contract SymbioticStaking is ) external { _checkTransmitterRegistration(_captureTimestamp); - _checkValidity(_index, _numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); + _checkValidity(_index, _numOfTxs, _captureTimestamp, STAKE_SNAPSHOT); _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); @@ -103,12 +103,12 @@ contract SymbioticStaking is Struct.VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (Struct.VaultSnapshot[])); _updateSnapshotInfo(_captureTimestamp, _vaultSnapshots); - _updateTxCountInfo(_numOfTxs, _captureTimestamp, VAULT_SNAPSHOT); + _updateTxCountInfo(_numOfTxs, _captureTimestamp, STAKE_SNAPSHOT); - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT_MASK; + submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; } if (_isCompleteStatus(_captureTimestamp)) { @@ -123,21 +123,21 @@ contract SymbioticStaking is bytes memory _SlashResultData, bytes memory _signature ) external { + _checkTransmitterRegistration(_captureTimestamp); + _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT); _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultData, _signature); - //? is there any need to store the slash result data? - //? is there any need to even lock stake here? - anyway it'll just be slashed in L1 Struct.JobSlashed[] memory _jobSlashed = abi.decode(_SlashResultData, (Struct.JobSlashed[])); // _updateSlashResultDataInfo(_captureTimestamp, _jobSlashed); _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][VAULT_SNAPSHOT]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= VAULT_SNAPSHOT; + submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT; } if (_isCompleteStatus(_captureTimestamp)) { @@ -218,14 +218,14 @@ contract SymbioticStaking is Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; require(_index == snapshot.idxToSubmit, "Invalid index"); - require(snapshot.idxToSubmit < snapshot.numOfTxs, "Snapshot fully submitted already"); - require(snapshot.numOfTxs == _numOfTxs, "Invalid length"); + require(_index < snapshot.numOfTxs, "Invalid index"); + require(snapshot.numOfTxs == _numOfTxs, "Invalid numOfTxs"); bytes32 mask; - if (_type == VAULT_SNAPSHOT) mask = VAULT_SNAPSHOT_MASK; + if (_type == STAKE_SNAPSHOT) mask = STAKE_SNAPSHOT_MASK; else if (_type == SLASH_RESULT) mask = SLASH_RESULT_MASK; - require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Snapshot fully submitted already"); + require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Already submitted"); } function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { From 9513350bd80d7febe3a4428462fef1413b4d5e47 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 30 Sep 2024 17:34:10 +0900 Subject: [PATCH 113/158] logic when operatorStakeAmount < operatorLockAmount --- contracts/staking/l2_contracts/SymbioticStaking.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index d7babf7..35ce323 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -297,7 +297,9 @@ contract SymbioticStaking is } function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token] - operatorLockedAmounts[_operator][_token]; + uint256 operatorStakeAmount = operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; + uint256 operatorLockedAmount = operatorLockedAmounts[_operator][_token]; + return operatorStakeAmount > operatorLockedAmount ? operatorStakeAmount - operatorLockedAmount : 0; } function isSupportedToken(address _token) public view returns (bool) { From 4b3dd149da0c815d782ccc6acf128307dd25dda0 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 1 Oct 2024 20:00:28 +0900 Subject: [PATCH 114/158] add logic for inflation reward emission --- .../interfaces/staking/INativeStaking.sol | 1 + .../interfaces/staking/IRewardDistributor.sol | 4 +- .../interfaces/staking/IStakingManager.sol | 4 +- contracts/interfaces/staking/IStakingPool.sol | 4 +- .../interfaces/staking/ISymbioticStaking.sol | 2 - contracts/staking/l2_contracts/JobManager.sol | 100 ++++++++++-- .../staking/l2_contracts/NativeStaking.sol | 120 +++++++++------ .../l2_contracts/NativeStakingReward.sol | 45 ++++-- .../staking/l2_contracts/StakingExample.sol | 144 ------------------ .../staking/l2_contracts/StakingManager.sol | 34 ++++- .../staking/l2_contracts/SymbioticStaking.sol | 56 +++++-- .../l2_contracts/SymbioticStakingReward.sol | 15 +- 12 files changed, 282 insertions(+), 247 deletions(-) delete mode 100644 contracts/staking/l2_contracts/StakingExample.sol diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 166989e..8747cb2 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.26; import {IStakingPool} from "../staking/IStakingPool.sol"; interface INativeStaking is IStakingPool { + function getStakeTokenList() external view returns (address[] memory); // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); diff --git a/contracts/interfaces/staking/IRewardDistributor.sol b/contracts/interfaces/staking/IRewardDistributor.sol index 7b1b9bd..bdb7fc2 100644 --- a/contracts/interfaces/staking/IRewardDistributor.sol +++ b/contracts/interfaces/staking/IRewardDistributor.sol @@ -3,5 +3,7 @@ pragma solidity ^0.8.26; interface IRewardDistributor { - function addReward(address _stakeToken, address operator, address _rewardToken, uint256 _amount) external; + function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external; + + function addInflationReward(address _operator, uint256 _amount) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 2260159..93a20df 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -7,7 +7,9 @@ pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; - function onJobCompletion(uint256 jobId, address operator, uint256 feePaid) external; + function onJobCompletion(uint256 jobId, address operator, uint256 feePaid, uint256 inflationReward) external; function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; + + function distributeInflationReward(address operator, uint256 rewardAmount) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index f1e022b..c2b0196 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -10,11 +10,13 @@ interface IStakingPool { function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external; // Staking Manager only + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external; // Staking Manager only function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256); function rewardDistributor() external view returns (address); + + function distributeInflationReward(address _operator, uint256 _rewardAmount) external; // Staking Manager only } \ No newline at end of file diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index cee7c87..a108784 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -6,8 +6,6 @@ import {IStakingPool} from "./IStakingPool.sol"; interface ISymbioticStaking is IStakingPool { // function stakeOf(address _operator, address _token) external view returns (uint256); - - // event OperatorSnapshotSubmitted // event VaultSnapshotSubmitted diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 2f7eb55..71243b2 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -14,6 +14,8 @@ import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + /* JobManager contract is responsible for creating and managing jobs. Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. @@ -32,10 +34,26 @@ contract JobManager is address public stakingManager; address public feeToken; + address public inflationRewardToken; uint256 public jobDuration; uint256 public totalFeeStored; // TODO: check if needed + uint256 inflationRewardEpochSize; + uint256 inflationRewardPerEpoch; + + // epochs in which operator has done jobs + mapping(address operator => uint256[] epochs) operatorJobCompletionEpochs; + // idx of operatorJobCompletionEpochs, inflationReward distribution should be reflected from this idx + mapping(address operator => uint256 idx) inflationRewardEpochBeginIdx; + + // count of jobs done by operator in an epoch + mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCount; + // total count of jobs done in an epoch + mapping(uint256 epoch => uint256 totalCount) totalJobCount; + + /*======================================== Init ========================================*/ + function initialize(address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) public initializer @@ -52,6 +70,8 @@ contract JobManager is jobDuration = _jobDuration; } + /*======================================== Job ========================================*/ + // TODO: check paramter for job details function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external @@ -82,9 +102,13 @@ contract JobManager is _verifyProof(_jobId, _proof); + uint256 feePaid = jobs[_jobId].feePaid; + uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); + // send fee and unlock stake - IERC20(feeToken).safeTransfer(stakingManager, jobs[_jobId].feePaid); // TODO: make RewardDistributor pull fee from JobManager - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, jobs[_jobId].feePaid); + IERC20(feeToken).safeTransfer(stakingManager, feePaid); // TODO: make RewardDistributor pull fee from JobManager + IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); } /** @@ -93,20 +117,17 @@ contract JobManager is function submitProofs(uint256[] calldata _jobIds, bytes[] calldata _proofs) external nonReentrant { require(_jobIds.length == _proofs.length, "Invalid Length"); - // TODO: close job and distribute rewards - uint256 len = _jobIds.length; for (uint256 idx = 0; idx < len; idx++) { - uint256 _jobId = _jobIds[idx]; - require(block.timestamp <= jobs[_jobId].deadline, "Job Expired"); - - _verifyProof(_jobId, _proofs[idx]); - - // TODO: let onJobCompletion also accept array of jobIds - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, jobs[_jobId].feePaid); // unlock stake + submitProof(_jobIds[idx], _proofs[idx]); // TODO: optimize } } + /*======================================== Fee ========================================*/ + + /// @notice refund fee to the job requester + /// @dev most likely called by the requester when job is not completed + /// @dev or when the job is slashed and the slash result is submitted in SymbioticStaking contract function refundFee(uint256 _jobId) external nonReentrant { if (jobs[_jobId].feePaid > 0) { require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); @@ -120,12 +141,69 @@ contract JobManager is } } + /*======================================== Inflation Reward ========================================*/ + + /// @notice update inflation reward for operator + /// @dev can be called by anyone, but most likely when proof is submitted(when job is completed) by operator + /// @dev or inflation reward is claimed in a RewardDistributor + function updateInflationReward(address _operator) external { + uint256 pendingInflationReward = _updateInflationReward(_operator); + + if(pendingInflationReward > 0) { + // send reward to StakingManager + IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); + // and distribute + IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); + } + } + + /*======================================== Internal functions ========================================*/ + function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { // TODO: verify proof // TODO: emit event } + /// @notice update pending inflation reward for operator + function _updateInflationReward(address _operator) internal returns(uint256 pendingInflationReward) { + // check if operator has completed any job + if (operatorJobCompletionEpochs[_operator].length == 0) return 0; + + // list of epochs in which operator has completed jobs + uint256[] storage completedEpochs = operatorJobCompletionEpochs[_operator]; + + // first epoch which the reward has not been distributed + uint256 _beginIdx = inflationRewardEpochBeginIdx[_operator]; + + // no job completed since last update + if(_beginIdx > completedEpochs.length) return 0; + + uint256 beginEpoch = completedEpochs[_beginIdx]; + uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; + + // no pending reward if operator has already claimed reward until latest epoch + if(beginEpoch == currentEpoch) return 0; + + // update pending reward + uint256 rewardPerEpoch = inflationRewardPerEpoch; // cache + uint256 len = completedEpochs.length; + + for(uint256 idx = _beginIdx; idx < len; idx++) { + uint256 epoch = completedEpochs[idx]; + + // for last epoch in epoch array + if(idx == len - 1) { + // idx can be greater than actual length of epoch array by 1 + inflationRewardEpochBeginIdx[_operator] = epoch == currentEpoch ? idx : idx + 1; + } + + pendingInflationReward += Math.mulDiv(rewardPerEpoch, operatorJobCount[epoch][_operator], totalJobCount[epoch]); + } + + return pendingInflationReward; + } + /*======================================== Admin ========================================*/ function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 1e3d292..235fd25 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -16,6 +16,8 @@ import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; import {INativeStakingReward} from "../../interfaces/staking/INativeStakingReward.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + import {Struct} from "../../interfaces/staking/lib/Struct.sol"; contract NativeStaking is @@ -29,16 +31,16 @@ contract NativeStaking is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - - - EnumerableSet.AddressSet private tokenSet; + EnumerableSet.AddressSet private stakeTokenSet; address public rewardDistributor; address public stakingManager; address public feeRewardToken; + address public inflationRewardToken; /* Config */ - mapping(address token => uint256 lockAmount) public amountToLock; + mapping(address stakeToken => uint256 lockAmount) public amountToLock; // amount of token to lock for each job creation + mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% /* Stake */ // total staked amounts for each operator @@ -50,8 +52,8 @@ contract NativeStaking is mapping(uint256 jobId => Struct.NativeStakingLock lock) public jobLockedAmounts; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; - modifier onlySupportedToken(address _token) { - require(tokenSet.contains(_token), "Token not supported"); + modifier onlySupportedToken(address _stakeToken) { + require(stakeTokenSet.contains(_stakeToken), "Token not supported"); _; } @@ -70,43 +72,47 @@ contract NativeStaking is } // Staker should be able to choose an Operator they want to stake into - function stake(address _operator, address _token, uint256 _amount) + function stake(address _operator, address _stakeToken, uint256 _amount) external - onlySupportedToken(_token) + onlySupportedToken(_stakeToken) nonReentrant { // this check can be removed in the future to allow delegatedStake require(msg.sender == _operator, "Only operator can stake"); - IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_stakeToken).safeTransferFrom(msg.sender, address(this), _amount); - stakedAmounts[msg.sender][_operator][_token] += _amount; - operatorStakedAmounts[_operator][_token] += _amount; + stakedAmounts[msg.sender][_operator][_stakeToken] += _amount; + operatorStakedAmounts[_operator][_stakeToken] += _amount; // NativeStakingReward contract will read staking amount info from this contract // and update reward related states - INativeStakingReward(rewardDistributor).update(msg.sender, _token, _operator); + INativeStakingReward(rewardDistributor).update(msg.sender, _stakeToken, _operator); - emit Staked(msg.sender, _operator, _token, _amount, block.timestamp); + emit Staked(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } // This should update StakingManger's state - function requestStakeWithdrawal(address _operator, address _token, uint256 _amount) external nonReentrant { - require(getOperatorActiveStakeAmount(_operator, _token) >= _amount, "Insufficient stake"); + // TODO + function requestStakeWithdrawal(address _operator, address _stakeToken, uint256 _amount) external nonReentrant { + require(getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); - stakedAmounts[msg.sender][_operator][_token] -= _amount; - operatorStakedAmounts[_operator][_token] -= _amount; + stakedAmounts[msg.sender][_operator][_stakeToken] -= _amount; + operatorStakedAmounts[_operator][_stakeToken] -= _amount; - IERC20(_token).safeTransfer(msg.sender, _amount); + IERC20(_stakeToken).safeTransfer(msg.sender, _amount); - INativeStakingReward(rewardDistributor).update(msg.sender, _token, _operator); + INativeStakingReward(rewardDistributor).update(msg.sender, _stakeToken, _operator); - emit StakeWithdrawn(msg.sender, _operator, _token, _amount, block.timestamp); + emit StakeWithdrawn(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } - function withdrawStake(address _operator, address _token) external nonReentrant { - uint256 _amount = stakedAmounts[msg.sender][_operator][_token]; + function withdrawStake(address _operator, address _stakeToken) external nonReentrant { + require(msg.sender == _operator, "Only operator can withdraw stake"); + uint256 _amount = stakedAmounts[msg.sender][_operator][_stakeToken]; require(_amount > 0, "No stake to withdraw"); + + // TODO } /*======================================== Getters ========================================*/ @@ -120,20 +126,17 @@ contract NativeStaking is } function isSupportedToken(address _token) external view returns (bool) { - return tokenSet.contains(_token); + return stakeTokenSet.contains(_token); } - /*======================================== Admin ========================================*/ - function addToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(tokenSet.add(token), "Token already exists"); - - // TODO: emit event - } - - function removeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(tokenSet.remove(token), "Token does not exist"); + function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_isSupported) { + stakeTokenSet.add(_token); + } else { + stakeTokenSet.remove(_token); + } // TODO: emit event } @@ -171,17 +174,48 @@ contract NativeStaking is /// @notice unlock stake and distribute reward /// @dev called by StakingManager when job is completed - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external onlyStakingManager { + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.NativeStakingLock memory lock = jobLockedAmounts[_jobId]; if(lock.amount == 0) return; _unlockStake(_jobId, _operator, lock.token, lock.amount); - _distributeReward(lock.token, _operator, feeRewardToken, _feeRewardAmount); + + // distribute fee reward + if(_feeRewardAmount > 0){ + _distributeFeeReward(lock.token, _operator, _feeRewardAmount); + } + + if(_inflationRewardAmount > 0) { + _distributeInflationReward(_operator, _inflationRewardAmount); + } // TODO: emit event } + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); // TODO: consolidate with inflation reward + } + + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { + if(_rewardAmount == 0) return; + + uint256 len = stakeTokenSet.length(); + for(uint256 i = 0; i < len; i++) { + _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization + } + } + + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); + } + + function _distributeInflationReward(address _operator, uint256 _amount) internal { + IERC20(inflationRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); // TODO: consolidate with fee reward + } + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { @@ -197,26 +231,24 @@ contract NativeStaking is } } + function getStakeTokenList() external view returns(address[] memory) { + return stakeTokenSet.values(); + } + function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { operatorLockedAmounts[_operator][_stakeToken] -= _amount; delete jobLockedAmounts[_jobId]; } - function _distributeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { - IERC20(_rewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addReward(_stakeToken, _operator, _rewardToken, _amount); - } - - function _selectTokenToLock() internal view returns(address) { - require(tokenSet.length() > 0, "No supported token"); + require(stakeTokenSet.length() > 0, "No supported token"); uint256 idx; - if (tokenSet.length() > 1) { + if (stakeTokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - idx = randomNumber % tokenSet.length(); + idx = randomNumber % stakeTokenSet.length(); } - return tokenSet.at(idx); + return stakeTokenSet.at(idx); } /*======================================== Overrides ========================================*/ diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 52b3161..dc6d585 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -31,6 +31,8 @@ contract NativeStakingReward is address public feeRewardToken; address public inflationRewardToken; + mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% + // reward is accrued per operator mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) rewards; // rewardTokens amount per stakeToken @@ -66,19 +68,22 @@ contract NativeStakingReward is } - function addReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) public onlyNativeStaking { - rewards[_stakeToken][_operator][_rewardToken] += _amount; - _update(address(0), _stakeToken, _operator, _rewardToken); - } - - function updateFeeReward(address account, address _stakeToken, address _operator) public onlyNativeStaking { - _update(account, _stakeToken, _operator, feeRewardToken); + function addFeeReward(address _stakeToken, address _operator, uint256 _amount) public onlyNativeStaking { + rewards[_stakeToken][_operator][feeRewardToken] += _amount; + _update(address(0), _stakeToken, _operator, feeRewardToken); } - function updateInflationReward(address account, address _stakeToken, address _operator) public onlyNativeStaking { - _update(account, _stakeToken, _operator, inflationRewardToken); - } + function addInflationReward(address _operator, uint256 _amount) public onlyNativeStaking { + address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); + + for(uint256 i = 0; i < stakeTokens.length; i++) { + rewards[stakeTokens[i]][_operator][inflationRewardToken] += _amount.mulDiv(inflationRewardShare[stakeTokens[i]], 1e18); + _update(address(0), stakeTokens[i], _operator, inflationRewardToken); + } + // TODO: emit event + } + function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _operator, _rewardToken); rewardPerTokens[_stakeToken][_operator][_rewardToken] = currentRewardPerToken; @@ -125,6 +130,26 @@ contract NativeStakingReward is //-------------------------------- NativeStaking end --------------------------------// + //-------------------------------- Admin start --------------------------------// + + function setInflationRewardShare(address[] calldata stakeTokens, uint256[] calldata shares) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokens.length == shares.length, "Invalid Length"); + + uint256 sum = 0; + for (uint256 i = 0; i < shares.length; i++) { + require(INativeStaking(nativeStaking).isSupportedToken(stakeTokens[i]), "Invalid Token"); + + inflationRewardShare[stakeTokens[i]] = shares[i]; + sum += shares[i]; + } + + require(sum == 1e18, "Invalid Shares"); + + // TODO: emit event + } + + //-------------------------------- Admin nd --------------------------------// + //-------------------------------- Overrides start --------------------------------// function setNativeStaking(address _nativeStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { nativeStaking = _nativeStaking; diff --git a/contracts/staking/l2_contracts/StakingExample.sol b/contracts/staking/l2_contracts/StakingExample.sol deleted file mode 100644 index 1dcf5d8..0000000 --- a/contracts/staking/l2_contracts/StakingExample.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -contract StakingRewards { - IERC20 public immutable stakingToken; - IERC20 public immutable rewardsToken; - - address public owner; - - // Duration of rewards to be paid out (in seconds) - uint256 public duration; - // Timestamp of when the rewards finish - uint256 public finishAt; - // Minimum of last updated time and reward finish time - uint256 public updatedAt; - // Reward to be paid out per second - uint256 public rewardRate; - // Sum of (reward rate * dt * 1e18 / total supply) - uint256 public rewardPerTokenStored; - // User address => rewardPerTokenStored - mapping(address => uint256) public userRewardPerTokenPaid; - // User address => rewards to be claimed - mapping(address => uint256) public rewards; - - // Total staked - uint256 public totalSupply; - // User address => staked amount - mapping(address => uint256) public balanceOf; - - constructor(address _stakingToken, address _rewardToken) { - owner = msg.sender; - stakingToken = IERC20(_stakingToken); - rewardsToken = IERC20(_rewardToken); - } - - modifier onlyOwner() { - require(msg.sender == owner, "not authorized"); - _; - } - - modifier updateReward(address _account) { - rewardPerTokenStored = rewardPerToken(); - updatedAt = lastTimeRewardApplicable(); - - if (_account != address(0)) { - rewards[_account] = earned(_account); - userRewardPerTokenPaid[_account] = rewardPerTokenStored; - } - - _; - } - - function lastTimeRewardApplicable() public view returns (uint256) { - return _min(finishAt, block.timestamp); - } - - function rewardPerToken() public view returns (uint256) { - if (totalSupply == 0) { - return rewardPerTokenStored; - } - - return rewardPerTokenStored - + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) - / totalSupply; - } - - function stake(uint256 _amount) external updateReward(msg.sender) { - require(_amount > 0, "amount = 0"); - stakingToken.transferFrom(msg.sender, address(this), _amount); - balanceOf[msg.sender] += _amount; - totalSupply += _amount; - } - - function withdraw(uint256 _amount) external updateReward(msg.sender) { - require(_amount > 0, "amount = 0"); - balanceOf[msg.sender] -= _amount; - totalSupply -= _amount; - stakingToken.transfer(msg.sender, _amount); - } - - function earned(address _account) public view returns (uint256) { - return ( - ( - balanceOf[_account] - * (rewardPerToken() - userRewardPerTokenPaid[_account]) - ) / 1e18 - ) + rewards[_account]; - } - - function getReward() external updateReward(msg.sender) { - uint256 reward = rewards[msg.sender]; - if (reward > 0) { - rewards[msg.sender] = 0; - rewardsToken.transfer(msg.sender, reward); - } - } - - function setRewardsDuration(uint256 _duration) external onlyOwner { - require(finishAt < block.timestamp, "reward duration not finished"); - duration = _duration; - } - - function notifyRewardAmount(uint256 _amount) - external - onlyOwner - updateReward(address(0)) - { - if (block.timestamp >= finishAt) { - rewardRate = _amount / duration; - } else { - uint256 remainingRewards = (finishAt - block.timestamp) * rewardRate; - rewardRate = (_amount + remainingRewards) / duration; - } - - require(rewardRate > 0, "reward rate = 0"); - require( - rewardRate * duration <= rewardsToken.balanceOf(address(this)), - "reward amount > balance" - ); - - finishAt = block.timestamp + duration; - updatedAt = block.timestamp; - } - - function _min(uint256 x, uint256 y) private pure returns (uint256) { - return x <= y ? x : y; - } -} - -interface IERC20 { - function totalSupply() external view returns (uint256); - function balanceOf(address account) external view returns (uint256); - function transfer(address recipient, uint256 amount) - external - returns (bool); - function allowance(address owner, address spender) - external - view - returns (uint256); - function approve(address spender, uint256 amount) external returns (bool); - function transferFrom(address sender, address recipient, uint256 amount) - external - returns (bool); -} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 39af33b..167625e 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -70,20 +70,42 @@ contract StakingManager is } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feePaid) external onlyJobManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feePaid, uint256 _inflationRewardAmount) external onlyJobManager { uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - uint256 feeRewardAmount = _calcFeeRewardAmount(pool, _feePaid); - IERC20(feeToken).safeTransfer(pool, feeRewardAmount); - IStakingPool(pool).unlockStake(_jobId, _operator, feeRewardAmount); + (uint256 poolFeeRewardAmount, uint256 poolInflationRewardAmount) = _calcRewardAmount(pool, _feePaid, _inflationRewardAmount); + + IERC20(feeToken).safeTransfer(pool, poolFeeRewardAmount); + IERC20(inflationRewardToken).safeTransfer(pool, poolInflationRewardAmount); + IStakingPool(pool).unlockStake(_jobId, _operator, poolFeeRewardAmount, _inflationRewardAmount); } // TODO: emit event } - function _calcFeeRewardAmount(address _pool, uint256 _feePaid) internal view returns (uint256) { - return Math.mulDiv(_feePaid, poolConfig[_pool].weight, 1e18); + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyJobManager { + if(_rewardAmount == 0) return; + + uint256 len = stakingPoolSet.length(); + for (uint256 i = 0; i < len; i++) { + address pool = stakingPoolSet.at(i); + + (, uint256 poolRewardAmount) = _calcRewardAmount(pool, 0, _rewardAmount); + IERC20(inflationRewardToken).safeTransfer(pool, poolRewardAmount); + + // TODO + IStakingPool(pool).distributeInflationReward(_operator, poolRewardAmount); + } + } + + function _calcRewardAmount(address _pool, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) internal view returns (uint256, uint256) { + uint256 poolWeight = poolConfig[_pool].weight; + + uint256 poolFeeRewardAmount = _feeRewardAmount > 0 ? Math.mulDiv(_feeRewardAmount, poolWeight, 1e18) : 0; + uint256 poolInflationRewardAmount = _inflationRewardAmount > 0 ? Math.mulDiv(_inflationRewardAmount, poolWeight, 1e18) : 0; + + return (poolFeeRewardAmount, poolInflationRewardAmount); } /// @notice called by SymbioticStaking contract when slash result is submitted diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 35ce323..3dc7afd 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -40,14 +40,17 @@ contract SymbioticStaking is bytes32 public constant STAKE_SNAPSHOT = keccak256("STAKE_SNAPSHOT"); bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); - EnumerableSet.AddressSet tokenSet; + EnumerableSet.AddressSet stakeTokenSet; address public feeRewardToken; + address public inflationRewardToken; address public stakingManager; address public rewardDistributor; /* Config */ mapping(address token => uint256 amount) public amountToLock; + mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% + /* Symbiotic Snapshot */ mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received @@ -162,14 +165,19 @@ contract SymbioticStaking is // TODO: emit event } - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external onlyStakingManager { + function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; uint256 transmitterComissionRate = confirmedTimestamps[confirmedTimestamps.length - 1].transmitterComissionRate; uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; - IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); // reward transmitter - _distributeReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); + if(feeRewardRemaining > 0) { + _distributeFeeReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); + } + + if(_inflationRewardAmount > 0) { + _distributeInflationReward(_operator, _inflationRewardAmount); + } delete lockInfo[_jobId]; operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; @@ -177,9 +185,27 @@ contract SymbioticStaking is // TODO: emit event } - function _distributeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { - IERC20(_rewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addReward(_stakeToken, _operator, _rewardToken, _amount); + function _distributeFeeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); + } + + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { + if(_rewardAmount == 0) return; + + uint256 len = stakeTokenSet.length(); + for(uint256 i = 0; i < len; i++) { + _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization + } + } + + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); + } + + function _distributeInflationReward(address _operator, uint256 _amount) internal { + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); } function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { @@ -198,14 +224,14 @@ contract SymbioticStaking is } function _selectLockToken() internal view returns(address) { - require(tokenSet.length() > 0, "No supported token"); + require(stakeTokenSet.length() > 0, "No supported token"); uint256 idx; - if (tokenSet.length() > 1) { + if (stakeTokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - idx = randomNumber % tokenSet.length(); + idx = randomNumber % stakeTokenSet.length(); } - return tokenSet.at(idx); + return stakeTokenSet.at(idx); } /*======================================== Helpers ========================================*/ @@ -303,7 +329,7 @@ contract SymbioticStaking is } function isSupportedToken(address _token) public view returns (bool) { - return tokenSet.contains(_token); + return stakeTokenSet.contains(_token); } /*======================================== Admin ========================================*/ @@ -313,11 +339,11 @@ contract SymbioticStaking is // TODO: emit event } - function setSupportedToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { if (_isSupported) { - require(tokenSet.add(_token), "Token already exists"); + require(stakeTokenSet.add(_token), "Token already exists"); } else { - require(tokenSet.remove(_token), "Token does not exist"); + require(stakeTokenSet.remove(_token), "Token does not exist"); } } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 0227110..e89506c 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -35,11 +35,7 @@ contract SymbioticStakingReward is // TODO: staking token enability should be pulled from SymbioticStaking contract - EnumerableSet.AddressSet private _rewardTokenSet; - - //? what should be done when stake is locked? - // -> just update totalStakeAmount and rewardPerToken? - // -> does this even affect anything? + EnumerableSet.AddressSet private _rewardTokenSet; // TODO: remove this, only feeRewardToken and inflationRewardToken will be used /* rewardToken: Fee Reward, Inflation Reward @@ -51,7 +47,6 @@ contract SymbioticStakingReward is mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public totalStakeAmounts; - // TODO: this should be pulled from SymbioticStaking contract // locked amount for each stakeToken upon job creation mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public lockedStakeAmounts; @@ -165,8 +160,6 @@ contract SymbioticStakingReward is //-------------------------------- Update start --------------------------------// function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { - require(isSupportedRewardToken(_rewardToken), "unsupported reward token"); - // update rewardPerToken uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; @@ -216,6 +209,8 @@ contract SymbioticStakingReward is _rewardTokenSet.remove(_rewardToken); } + + //-------------------------------- Admin end --------------------------------// //-------------------------------- Getter start --------------------------------// @@ -234,10 +229,6 @@ contract SymbioticStakingReward is return _rewardTokens; } - function isSupportedRewardToken(address _rewardToken) public view returns (bool) { - return _rewardTokenSet.contains(_rewardToken); - } - function totalStakeAmountsActive(address _stakeToken) public view returns (uint256) { uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); return totalStakeAmounts[latestConfirmedTimestamp][_stakeToken] From 359f637afb16cc571116fec40f1058c19bebdf62 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 1 Oct 2024 20:26:37 +0900 Subject: [PATCH 115/158] refactor reward distribution logic --- contracts/staking/l2_contracts/JobManager.sol | 16 +++++++++++++++- .../staking/l2_contracts/SymbioticStaking.sol | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 71243b2..9956865 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -109,6 +109,8 @@ contract JobManager is IERC20(feeToken).safeTransfer(stakingManager, feePaid); // TODO: make RewardDistributor pull fee from JobManager IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); + + _updateJobCompletionEpoch(_jobId); } /** @@ -119,7 +121,19 @@ contract JobManager is uint256 len = _jobIds.length; for (uint256 idx = 0; idx < len; idx++) { - submitProof(_jobIds[idx], _proofs[idx]); // TODO: optimize + uint256 jobId = _jobIds[idx]; + submitProof(jobId, _proofs[idx]); // TODO: optimize + + _updateJobCompletionEpoch(jobId); + } + } + + function _updateJobCompletionEpoch(uint256 _jobId) internal { + uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; + uint256 len = operatorJobCompletionEpochs[jobs[_jobId].operator].length; + + if(len > 0 && operatorJobCompletionEpochs[jobs[_jobId].operator][len - 1] != currentEpoch) { + operatorJobCompletionEpochs[jobs[_jobId].operator].push(currentEpoch); } } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 3dc7afd..8cfb2c7 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -172,7 +172,7 @@ contract SymbioticStaking is uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; if(feeRewardRemaining > 0) { - _distributeFeeReward(lock.stakeToken, _operator, feeRewardToken, feeRewardRemaining); + _distributeFeeReward(lock.stakeToken, _operator, feeRewardRemaining); } if(_inflationRewardAmount > 0) { @@ -185,7 +185,7 @@ contract SymbioticStaking is // TODO: emit event } - function _distributeFeeReward(address _stakeToken, address _operator, address _rewardToken, uint256 _amount) internal { + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); } From 9671f96df6e21f29af1016b230928161f4908d5b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 1 Oct 2024 21:30:38 +0900 Subject: [PATCH 116/158] refactor _update logic --- contracts/interfaces/staking/lib/Struct.sol | 5 +- .../l2_contracts/NativeStakingReward.sol | 61 +++++++++++-------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/interfaces/staking/lib/Struct.sol index 55df3c4..3b506d1 100644 --- a/contracts/interfaces/staking/lib/Struct.sol +++ b/contracts/interfaces/staking/lib/Struct.sol @@ -58,5 +58,8 @@ library Struct { uint256 weight; bool enabled; } - + struct RewardPerToken { + uint256 rewardPerToken; + uint256 lastUpdatedTimestamp; + } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index dc6d585..d8fe83c 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -15,6 +15,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; +import {Struct} from "../../interfaces/staking/lib/Struct.sol"; contract NativeStakingReward is ContextUpgradeable, @@ -36,10 +37,10 @@ contract NativeStakingReward is // reward is accrued per operator mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) rewards; // rewardTokens amount per stakeToken - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) rewardPerTokens; + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => Struct.RewardPerToken rewardPerToken))) rewardPerTokens; mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) userRewardPerTokenPaid; - mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount)))) rewardAccrued; + mapping(address account => mapping(address rewardToken => uint256 amount)) rewardAccrued; modifier onlyNativeStaking() { require(msg.sender == nativeStaking, "Only NativeStaking"); @@ -62,38 +63,49 @@ contract NativeStakingReward is } //-------------------------------- Init end --------------------------------// - //-------------------------------- NativeStaking start --------------------------------// - - function claimReward(address token) public { - - } - - function addFeeReward(address _stakeToken, address _operator, uint256 _amount) public onlyNativeStaking { + //-------------------------------- Staking start --------------------------------// + function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external onlyNativeStaking { rewards[_stakeToken][_operator][feeRewardToken] += _amount; _update(address(0), _stakeToken, _operator, feeRewardToken); } - function addInflationReward(address _operator, uint256 _amount) public onlyNativeStaking { + function addInflationReward(address _operator, uint256 _amount) external onlyNativeStaking { address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); + address _inflationRewardToken = inflationRewardToken; // cache for(uint256 i = 0; i < stakeTokens.length; i++) { - rewards[stakeTokens[i]][_operator][inflationRewardToken] += _amount.mulDiv(inflationRewardShare[stakeTokens[i]], 1e18); - _update(address(0), stakeTokens[i], _operator, inflationRewardToken); + rewards[stakeTokens[i]][_operator][_inflationRewardToken] += _amount.mulDiv(inflationRewardShare[stakeTokens[i]], 1e18); + _update(address(0), stakeTokens[i], _operator, _inflationRewardToken); } // TODO: emit event } + + function claimReward(address operator) external { + address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); + + for(uint256 i = 0; i < stakeTokens.length; i++) { + _update(msg.sender, stakeTokens[i], operator, feeRewardToken); + _update(msg.sender, stakeTokens[i], operator, inflationRewardToken); + } + + IERC20(feeRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][feeRewardToken]); + IERC20(inflationRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][inflationRewardToken]); + } function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { - uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _operator, _rewardToken); - rewardPerTokens[_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + rewardPerTokens[_stakeToken][_operator][_rewardToken] = Struct.RewardPerToken(_rewardPerToken(_stakeToken, _operator, _rewardToken), block.timestamp); if(account != address(0)) { - rewardAccrued[account][_stakeToken][_operator][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); - userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = currentRewardPerToken; + _updateUser(account, _stakeToken, _operator, _rewardToken); } } + function _updateUser(address account, address _stakeToken, address _operator, address _rewardToken) internal { + rewardAccrued[account][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); + userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = _rewardPerToken(_stakeToken, _operator, _rewardToken); + } + function _pendingReward(address account, address _stakeToken, address operator, address _rewardToken) internal view returns (uint256) { uint256 rewardPerTokenPaid = userRewardPerTokenPaid[account][_stakeToken][operator][_rewardToken]; uint256 rewardPerToken = _rewardPerToken(_stakeToken, operator, _rewardToken); @@ -103,13 +115,17 @@ contract NativeStakingReward is } function _rewardPerToken(address _stakeToken, address _operator, address _rewardToken) internal view returns (uint256) { + if(rewardPerTokens[_stakeToken][_operator][_rewardToken].lastUpdatedTimestamp == block.timestamp) { + return rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken; + } + uint256 operatorStakeAmount = _getOperatorStakeAmount(_stakeToken, _operator); uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; // TODO: make sure decimal is 18 return operatorStakeAmount == 0 - ? rewardPerTokens[_stakeToken][_operator][_rewardToken] - : rewardPerTokens[_stakeToken][_operator][_rewardToken] + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); + ? rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken + : rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); } function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { @@ -120,15 +136,8 @@ contract NativeStakingReward is // return INativeStaking(nativeStaking).getUserStakeAmount(account, token, operator); } - function _getDelegatedStakeActive(address account, address token, address operator) - internal - view - returns (uint256) - { - // return INativeStaking(nativeStaking).getDelegatedStakeActive(account, token, operator); - } - //-------------------------------- NativeStaking end --------------------------------// + //-------------------------------- Staking end --------------------------------// //-------------------------------- Admin start --------------------------------// From 5ea15b552423841f8517f5ffe68a85f285de2a25 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 1 Oct 2024 22:50:43 +0900 Subject: [PATCH 117/158] update reward claim logic --- contracts/interfaces/staking/IJobManager.sol | 2 ++ contracts/interfaces/staking/lib/Struct.sol | 2 +- .../l2_contracts/NativeStakingReward.sol | 22 ++++++++++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/contracts/interfaces/staking/IJobManager.sol b/contracts/interfaces/staking/IJobManager.sol index b9ba71f..40f9d81 100644 --- a/contracts/interfaces/staking/IJobManager.sol +++ b/contracts/interfaces/staking/IJobManager.sol @@ -7,4 +7,6 @@ interface IJobManager { function submitProof(uint256 jobId, bytes calldata proof) external; function refundFee(uint256 jobId) external; + + function updateInflationReward(address _operator) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/interfaces/staking/lib/Struct.sol index 3b506d1..79f5eb0 100644 --- a/contracts/interfaces/staking/lib/Struct.sol +++ b/contracts/interfaces/staking/lib/Struct.sol @@ -60,6 +60,6 @@ library Struct { } struct RewardPerToken { uint256 rewardPerToken; - uint256 lastUpdatedTimestamp; + uint256 lastUpdatedTimestamp; //? not sure if this actually saves gas } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index d8fe83c..62a1166 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -14,9 +14,11 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; import {Struct} from "../../interfaces/staking/lib/Struct.sol"; + contract NativeStakingReward is ContextUpgradeable, ERC165Upgradeable, @@ -28,6 +30,7 @@ contract NativeStakingReward is using Math for uint256; using SafeERC20 for IERC20; + address public jobManager; address public nativeStaking; address public feeRewardToken; address public inflationRewardToken; @@ -82,19 +85,31 @@ contract NativeStakingReward is } function claimReward(address operator) external { + IJobManager(jobManager).updateInflationReward(operator); + address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); + address _feeRewardToken = feeRewardToken; // cache + address _inflationRewardToken = inflationRewardToken; // cache + for(uint256 i = 0; i < stakeTokens.length; i++) { - _update(msg.sender, stakeTokens[i], operator, feeRewardToken); - _update(msg.sender, stakeTokens[i], operator, inflationRewardToken); + // inflation reward is already updated + _update(msg.sender, stakeTokens[i], operator, _feeRewardToken); } IERC20(feeRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][feeRewardToken]); IERC20(inflationRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][inflationRewardToken]); + + rewardAccrued[msg.sender][_feeRewardToken] = 0; + rewardAccrued[msg.sender][_inflationRewardToken] = 0; + + // TODO: emit event } function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { - rewardPerTokens[_stakeToken][_operator][_rewardToken] = Struct.RewardPerToken(_rewardPerToken(_stakeToken, _operator, _rewardToken), block.timestamp); + if(rewardPerTokens[_stakeToken][_operator][_rewardToken].lastUpdatedTimestamp < block.timestamp) { + rewardPerTokens[_stakeToken][_operator][_rewardToken] = Struct.RewardPerToken(_rewardPerToken(_stakeToken, _operator, _rewardToken), block.timestamp); + } if(account != address(0)) { _updateUser(account, _stakeToken, _operator, _rewardToken); @@ -115,6 +130,7 @@ contract NativeStakingReward is } function _rewardPerToken(address _stakeToken, address _operator, address _rewardToken) internal view returns (uint256) { + // gas savings if(rewardPerTokens[_stakeToken][_operator][_rewardToken].lastUpdatedTimestamp == block.timestamp) { return rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken; } From dec69fd63aa216105e29af475970fb690c22e42f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 3 Oct 2024 00:28:58 +0900 Subject: [PATCH 118/158] fix reward distribution logic --- .../interfaces/staking/INativeStaking.sol | 1 - .../staking/INativeStakingReward.sol | 4 +- .../interfaces/staking/IRewardDistributor.sol | 8 +- .../interfaces/staking/IStakingManager.sol | 2 +- contracts/interfaces/staking/IStakingPool.sol | 12 +- .../staking/lib => lib/staking}/Struct.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 105 +++++---- .../l2_contracts/NativeStakingReward.sol | 169 +------------- .../l2_contracts/RewardDistributor.sol | 220 ++++++++++++++++++ .../staking/l2_contracts/StakingManager.sol | 4 +- 11 files changed, 309 insertions(+), 220 deletions(-) rename contracts/{interfaces/staking/lib => lib/staking}/Struct.sol (97%) create mode 100644 contracts/staking/l2_contracts/RewardDistributor.sol diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 8747cb2..166989e 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.26; import {IStakingPool} from "../staking/IStakingPool.sol"; interface INativeStaking is IStakingPool { - function getStakeTokenList() external view returns (address[] memory); // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); diff --git a/contracts/interfaces/staking/INativeStakingReward.sol b/contracts/interfaces/staking/INativeStakingReward.sol index ee81648..3da3314 100644 --- a/contracts/interfaces/staking/INativeStakingReward.sol +++ b/contracts/interfaces/staking/INativeStakingReward.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.26; -interface INativeStakingReward { +import {IRewardDistributor} from "./IRewardDistributor.sol"; + +interface INativeStakingReward is IRewardDistributor { function update(address account, address _stakeToken, address _operator) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IRewardDistributor.sol b/contracts/interfaces/staking/IRewardDistributor.sol index bdb7fc2..ae89780 100644 --- a/contracts/interfaces/staking/IRewardDistributor.sol +++ b/contracts/interfaces/staking/IRewardDistributor.sol @@ -5,5 +5,11 @@ pragma solidity ^0.8.26; interface IRewardDistributor { function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external; - function addInflationReward(address _operator, uint256 _amount) external; + function addInflationReward(address _operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external; + + function onStakeUpdate(address _account, address _stakeToken, address _operator) external; + + function onClaimReward(address _account, address _operator) external; + + function onSlash() external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 93a20df..f78a208 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -import {Struct} from "./lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; pragma solidity ^0.8.26; diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index c2b0196..fc1af14 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -1,22 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {Struct} from "./lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; interface IStakingPool { function isSupportedToken(address _token) external view returns (bool); - // function getPoolStake(address _operator, address _token) external view returns (uint256); - function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external; // Staking Manager only + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external; // Staking Manager only function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256); + function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256); + function rewardDistributor() external view returns (address); function distributeInflationReward(address _operator, uint256 _rewardAmount) external; // Staking Manager only + + function getStakeTokenList() external view returns (address[] memory); + + function getStakeAmount(address staker, address stakeToken, address operator) external view returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/staking/lib/Struct.sol b/contracts/lib/staking/Struct.sol similarity index 97% rename from contracts/interfaces/staking/lib/Struct.sol rename to contracts/lib/staking/Struct.sol index 79f5eb0..50fcfa0 100644 --- a/contracts/interfaces/staking/lib/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -59,7 +59,7 @@ library Struct { bool enabled; } struct RewardPerToken { - uint256 rewardPerToken; + uint256 value; uint256 lastUpdatedTimestamp; //? not sure if this actually saves gas } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 9956865..4cc4fe0 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -12,7 +12,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; -import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 235fd25..46fe193 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -18,7 +18,7 @@ import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.so import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; contract NativeStaking is ContextUpgradeable, @@ -44,9 +44,9 @@ contract NativeStaking is /* Stake */ // total staked amounts for each operator - mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorStakedAmounts; + mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorstakeAmounts; // staked amount for each account - mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakedAmounts; + mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakeAmounts; /* Locked Stakes */ mapping(uint256 jobId => Struct.NativeStakingLock lock) public jobLockedAmounts; @@ -82,12 +82,10 @@ contract NativeStaking is IERC20(_stakeToken).safeTransferFrom(msg.sender, address(this), _amount); - stakedAmounts[msg.sender][_operator][_stakeToken] += _amount; - operatorStakedAmounts[_operator][_stakeToken] += _amount; + stakeAmounts[msg.sender][_operator][_stakeToken] += _amount; + operatorstakeAmounts[_operator][_stakeToken] += _amount; - // NativeStakingReward contract will read staking amount info from this contract - // and update reward related states - INativeStakingReward(rewardDistributor).update(msg.sender, _stakeToken, _operator); + INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); emit Staked(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } @@ -95,21 +93,21 @@ contract NativeStaking is // This should update StakingManger's state // TODO function requestStakeWithdrawal(address _operator, address _stakeToken, uint256 _amount) external nonReentrant { - require(getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); + require(_getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); - stakedAmounts[msg.sender][_operator][_stakeToken] -= _amount; - operatorStakedAmounts[_operator][_stakeToken] -= _amount; + stakeAmounts[msg.sender][_operator][_stakeToken] -= _amount; + operatorstakeAmounts[_operator][_stakeToken] -= _amount; IERC20(_stakeToken).safeTransfer(msg.sender, _amount); - INativeStakingReward(rewardDistributor).update(msg.sender, _stakeToken, _operator); + INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); emit StakeWithdrawn(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } function withdrawStake(address _operator, address _stakeToken) external nonReentrant { require(msg.sender == _operator, "Only operator can withdraw stake"); - uint256 _amount = stakedAmounts[msg.sender][_operator][_stakeToken]; + uint256 _amount = stakeAmounts[msg.sender][_operator][_stakeToken]; require(_amount > 0, "No stake to withdraw"); // TODO @@ -117,12 +115,28 @@ contract NativeStaking is /*======================================== Getters ========================================*/ - function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[_operator][_token]; + function getStakeTokenList() external view returns (address[] memory) { + return stakeTokenSet.values(); + } + + function getStakeAmount(address _account, address _operator, address _stakeToken) external view returns (uint256) { + return stakeAmounts[_account][_operator][_stakeToken]; } - function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; + function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256) { + return operatorstakeAmounts[_operator][_token]; + } + + function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256) { + return _getOperatorActiveStakeAmount(_operator, _token); + } + + function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { + return operatorstakeAmounts[_operator][_token]; + } + + function _getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; } function isSupportedToken(address _token) external view returns (bool) { @@ -163,7 +177,7 @@ contract NativeStaking is function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectTokenToLock(); uint256 _amountToLock = amountToLock[_token]; - require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); + require(_getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake jobLockedAmounts[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); @@ -174,7 +188,7 @@ contract NativeStaking is /// @notice unlock stake and distribute reward /// @dev called by StakingManager when job is completed - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.NativeStakingLock memory lock = jobLockedAmounts[_jobId]; if(lock.amount == 0) return; @@ -193,46 +207,49 @@ contract NativeStaking is // TODO: emit event } - function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); // TODO: consolidate with inflation reward + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { + uint256 len = _slashedJobs.length; + for (uint256 i = 0; i < len; i++) { + Struct.NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; + + uint256 lockedAmount = lock.amount; + if(lockedAmount == 0) continue; // if already slashed + + _unlockStake(_slashedJobs[i].jobId, _slashedJobs[i].operator, lock.token, lockedAmount); + IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); + + INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, lock.token, _slashedJobs[i].operator); + } + // TODO: emit event } function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { if(_rewardAmount == 0) return; - uint256 len = stakeTokenSet.length(); - for(uint256 i = 0; i < len; i++) { - _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization - } + _distributeInflationReward(_operator, _rewardAmount); } function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } - function _distributeInflationReward(address _operator, uint256 _amount) internal { - IERC20(inflationRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); // TODO: consolidate with fee reward + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); } - function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { - uint256 len = _slashedJobs.length; - for (uint256 i = 0; i < len; i++) { - Struct.NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; - - uint256 lockedAmount = lock.amount; - if(lockedAmount == 0) continue; // if already slashed - - _unlockStake(_slashedJobs[i].jobId, _slashedJobs[i].operator, lock.token, lockedAmount); - IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); - - // TODO: emit event + function _distributeInflationReward(address _operator, uint256 _rewardAmount) internal { + uint256 len = stakeTokenSet.length(); + address[] memory stakeTokens = stakeTokenSet.values(); + uint256[] memory rewardAmounts = new uint256[](len); + uint256 inflationRewardAmount; + for(uint256 i = 0; i < len; i++) { + rewardAmounts[i] = _calcInflationRewardAmount(stakeTokens[i], _rewardAmount); + inflationRewardAmount += rewardAmounts[i]; } - } - function getStakeTokenList() external view returns(address[] memory) { - return stakeTokenSet.values(); + IERC20(inflationRewardToken).safeTransfer(rewardDistributor, inflationRewardAmount); + IRewardDistributor(rewardDistributor).addInflationReward(_operator, stakeTokens, rewardAmounts); } function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 62a1166..5b9ff49 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -2,154 +2,28 @@ pragma solidity ^0.8.26; -import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; -import {Struct} from "../../interfaces/staking/lib/Struct.sol"; - +import {RewardDistributor} from "./RewardDistributor.sol"; contract NativeStakingReward is - ContextUpgradeable, - ERC165Upgradeable, - AccessControlUpgradeable, - ReentrancyGuardUpgradeable, - PausableUpgradeable, - UUPSUpgradeable + RewardDistributor { - using Math for uint256; - using SafeERC20 for IERC20; - - address public jobManager; - address public nativeStaking; - address public feeRewardToken; - address public inflationRewardToken; - - mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% - // reward is accrued per operator - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) rewards; - // rewardTokens amount per stakeToken - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => Struct.RewardPerToken rewardPerToken))) rewardPerTokens; - mapping(address account => mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)))) userRewardPerTokenPaid; - mapping(address account => mapping(address rewardToken => uint256 amount)) rewardAccrued; - - modifier onlyNativeStaking() { - require(msg.sender == nativeStaking, "Only NativeStaking"); - _; - } //-------------------------------- Init start --------------------------------// - function initialize(address _admin, address _nativeStaking) public initializer { - __Context_init_unchained(); - __ERC165_init_unchained(); - __AccessControl_init_unchained(); - __UUPSUpgradeable_init_unchained(); - __ReentrancyGuard_init_unchained(); - __ReentrancyGuard_init_unchained(); - - _grantRole(DEFAULT_ADMIN_ROLE, _admin); - - nativeStaking = _nativeStaking; - } + //-------------------------------- Init end --------------------------------// //-------------------------------- Staking start --------------------------------// - function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external onlyNativeStaking { - rewards[_stakeToken][_operator][feeRewardToken] += _amount; - _update(address(0), _stakeToken, _operator, feeRewardToken); - } - - function addInflationReward(address _operator, uint256 _amount) external onlyNativeStaking { - address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); - - address _inflationRewardToken = inflationRewardToken; // cache - for(uint256 i = 0; i < stakeTokens.length; i++) { - rewards[stakeTokens[i]][_operator][_inflationRewardToken] += _amount.mulDiv(inflationRewardShare[stakeTokens[i]], 1e18); - _update(address(0), stakeTokens[i], _operator, _inflationRewardToken); - } - - // TODO: emit event - } - - function claimReward(address operator) external { - IJobManager(jobManager).updateInflationReward(operator); - - address[] memory stakeTokens = INativeStaking(nativeStaking).getStakeTokenList(); - - address _feeRewardToken = feeRewardToken; // cache - address _inflationRewardToken = inflationRewardToken; // cache - - for(uint256 i = 0; i < stakeTokens.length; i++) { - // inflation reward is already updated - _update(msg.sender, stakeTokens[i], operator, _feeRewardToken); - } - - IERC20(feeRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][feeRewardToken]); - IERC20(inflationRewardToken).safeTransfer(msg.sender, rewardAccrued[msg.sender][inflationRewardToken]); - - rewardAccrued[msg.sender][_feeRewardToken] = 0; - rewardAccrued[msg.sender][_inflationRewardToken] = 0; - - // TODO: emit event - } - - function _update(address account, address _stakeToken, address _operator, address _rewardToken) internal { - if(rewardPerTokens[_stakeToken][_operator][_rewardToken].lastUpdatedTimestamp < block.timestamp) { - rewardPerTokens[_stakeToken][_operator][_rewardToken] = Struct.RewardPerToken(_rewardPerToken(_stakeToken, _operator, _rewardToken), block.timestamp); - } - - if(account != address(0)) { - _updateUser(account, _stakeToken, _operator, _rewardToken); - } - } - function _updateUser(address account, address _stakeToken, address _operator, address _rewardToken) internal { - rewardAccrued[account][_rewardToken] += _pendingReward(account, _stakeToken, _operator, _rewardToken); - userRewardPerTokenPaid[account][_stakeToken][_operator][_rewardToken] = _rewardPerToken(_stakeToken, _operator, _rewardToken); - } - - function _pendingReward(address account, address _stakeToken, address operator, address _rewardToken) internal view returns (uint256) { - uint256 rewardPerTokenPaid = userRewardPerTokenPaid[account][_stakeToken][operator][_rewardToken]; - uint256 rewardPerToken = _rewardPerToken(_stakeToken, operator, _rewardToken); - uint256 userStakeAmount = _getUserStakeAmount(account, _stakeToken, operator); - - return userStakeAmount.mulDiv(rewardPerToken - rewardPerTokenPaid, 1e18); - } - - function _rewardPerToken(address _stakeToken, address _operator, address _rewardToken) internal view returns (uint256) { - // gas savings - if(rewardPerTokens[_stakeToken][_operator][_rewardToken].lastUpdatedTimestamp == block.timestamp) { - return rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken; - } - - uint256 operatorStakeAmount = _getOperatorStakeAmount(_stakeToken, _operator); - uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; - - // TODO: make sure decimal is 18 - return operatorStakeAmount == 0 - ? rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken - : rewardPerTokens[_stakeToken][_operator][_rewardToken].rewardPerToken + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); - } - - function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { - return INativeStaking(nativeStaking).getOperatorStakeAmount(_operator, _stakeToken); - } function _getUserStakeAmount(address account, address token, address operator) internal view returns (uint256) { - // return INativeStaking(nativeStaking).getUserStakeAmount(account, token, operator); + // return INativeStaking(stakingPool).getUserStakeAmount(account, token, operator); } @@ -157,46 +31,13 @@ contract NativeStakingReward is //-------------------------------- Admin start --------------------------------// - function setInflationRewardShare(address[] calldata stakeTokens, uint256[] calldata shares) public onlyRole(DEFAULT_ADMIN_ROLE) { - require(stakeTokens.length == shares.length, "Invalid Length"); - - uint256 sum = 0; - for (uint256 i = 0; i < shares.length; i++) { - require(INativeStaking(nativeStaking).isSupportedToken(stakeTokens[i]), "Invalid Token"); - inflationRewardShare[stakeTokens[i]] = shares[i]; - sum += shares[i]; - } - require(sum == 1e18, "Invalid Shares"); - - // TODO: emit event - } - - //-------------------------------- Admin nd --------------------------------// + //-------------------------------- Admin end --------------------------------// //-------------------------------- Overrides start --------------------------------// - function setNativeStaking(address _nativeStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { - nativeStaking = _nativeStaking; - - // TODO: emit event - } //-------------------------------- Overrides end --------------------------------// - //-------------------------------- Overrides start --------------------------------// - - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC165Upgradeable, AccessControlUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId); - } - function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} - - //-------------------------------- Overrides end --------------------------------// } diff --git a/contracts/staking/l2_contracts/RewardDistributor.sol b/contracts/staking/l2_contracts/RewardDistributor.sol new file mode 100644 index 0000000..3858e14 --- /dev/null +++ b/contracts/staking/l2_contracts/RewardDistributor.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {Struct} from "../../lib/staking/Struct.sol"; + +abstract contract RewardDistributor is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + UUPSUpgradeable, + IRewardDistributor +{ + using Math for uint256; + using SafeERC20 for IERC20; + + address public jobManager; + address public stakingPool; + + address public feeRewardToken; + address public inflationRewardToken; + + // mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% + + // reward is accrued per operator + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) + rewards; + // rewardTokens amount per stakeToken + mapping( + address stakeToken + => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken)) + ) rewardPerTokenStored; + + mapping( + address account + => mapping( + address stakeToken + => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)) + ) + ) rewardPerTokenPaids; + + mapping(address account => mapping(address rewardToken => uint256 amount)) rewardAccrued; + + modifier onlyStakingPool() { + require(msg.sender == stakingPool, "Only StakingPool"); + _; + } + + function initialize( + address _admin, + address _jobManager, + address _stakingPool, + address _feeRewardToken, + address _inflationRewardToken + ) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + __ReentrancyGuard_init_unchained(); + __ReentrancyGuard_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + require(_admin != address(0), "Invalid Admin"); + require(_jobManager != address(0), "Invalid JobManager"); + require(_stakingPool != address(0), "Invalid StakingPool"); + require(_feeRewardToken != address(0), "Invalid FeeRewardToken"); + + jobManager = _jobManager; + stakingPool = _stakingPool; + feeRewardToken = _feeRewardToken; + inflationRewardToken = _inflationRewardToken; + } + + function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external onlyStakingPool { + rewards[_stakeToken][_operator][feeRewardToken] += _amount; + + rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); + } + + /// @notice distribute inflation reward to all stakeTokens + /// @dev StakingPool should pass the list of stakeTokens and rewardAmount for each stakeToken + function addInflationReward(address _operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external onlyStakingPool { + address _inflationRewardToken = inflationRewardToken; // cache + for (uint256 i = 0; i < stakeTokens.length; i++) { + // distribute inflation reward to each stakeToken + rewards[stakeTokens[i]][_operator][_inflationRewardToken] += rewardAmounts[i]; + + // update rewardPerTokenStored for each stakeToken + rewardPerTokenStored[stakeTokens[i]][_operator][_inflationRewardToken] = _rewardPerTokenStored(stakeTokens[i], _operator, _inflationRewardToken); + } + // TODO: emit event + } + + /// @dev called when stake amount is updated in StakingPool + function onStakeUpdate(address _account, address _stakeToken, address _operator) external onlyStakingPool { + // update fee reward + rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); + + // update inflation reward + // TODO: check if there is any problem by not updating rewardPerTokenStored during Tx + _requestInflationRewardUpdate(_operator); + + uint256 rewardPerTokenStoredCurrent = rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken]; + address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); + + for(uint256 i = 0; i < stakeTokenList.length; i++) { + uint256 rewardPerTokenPaid = rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken]; + uint256 accountStakeAmount = _getStakeAmount(_account, stakeTokenList[i], _operator); + uint256 pendingReward = accountStakeAmount.mulDiv(rewardPerTokenStoredCurrent - rewardPerTokenPaid, 1e18); + + // update account's reward info + rewardAccrued[_account][inflationRewardToken] += pendingReward; + rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStoredCurrent; + + // update global rewardPerTokenStored + uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, stakeTokenList[i]); + rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken] += rewards[stakeTokenList[i]][_operator][inflationRewardToken].mulDiv(1e18, operatorStakeAmount); + } + } + + function onClaimReward(address _account, address _operator) external onlyStakingPool { + IERC20(feeRewardToken).safeTransfer(_account, rewardAccrued[_account][feeRewardToken]); + IERC20(inflationRewardToken).safeTransfer(_account, rewardAccrued[_account][inflationRewardToken]); + + rewardAccrued[_account][feeRewardToken] = 0; + rewardAccrued[_account][inflationRewardToken] = 0; + + address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); + for(uint256 i = 0; i < stakeTokenList.length; i++) { + rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][feeRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][feeRewardToken]; + rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken]; + } + } + + function onSlash() external onlyStakingPool { + // TODO + } + + function _requestInflationRewardUpdate(address _operator) internal { + // JobManager.updateInflationReward + // -> StakingManager.distributeInflationReward + // -> StakingPool.distributeInflationReward + // -> RewardDistributor.addInflationReward + IJobManager(jobManager).updateInflationReward(_operator); + } + + + /* Modification for _update */ + function _rewardPerTokenStored(address _stakeToken, address _operator, address _rewardToken) + internal + view + returns (uint256) + { + uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); + uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; + + // TODO: make sure decimal is 18 + return operatorStakeAmount == 0 + ? rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + : rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); + } + + function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { + return IStakingPool(stakingPool).getOperatorStakeAmount(_operator, _stakeToken); + } + + function _getStakeTokenList() internal view returns (address[] memory) { + return IStakingPool(stakingPool).getStakeTokenList(); + } + + function _getStakeAmount(address account, address _stakeToken, address _operator) internal view returns (uint256) { + return IStakingPool(stakingPool).getStakeAmount(account, _stakeToken, _operator); + } + + //-------------------------------- Admin start --------------------------------// + + function setStakingPool(address _stakingPool) public onlyRole(DEFAULT_ADMIN_ROLE) { + stakingPool = _stakingPool; + + // TODO: emit event + } + + //-------------------------------- Admin end --------------------------------// + + //-------------------------------- Overrides start --------------------------------// + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + //-------------------------------- Overrides end --------------------------------// +} diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 167625e..63a33cb 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -15,7 +15,7 @@ import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; -import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract StakingManager is @@ -79,7 +79,7 @@ contract StakingManager is IERC20(feeToken).safeTransfer(pool, poolFeeRewardAmount); IERC20(inflationRewardToken).safeTransfer(pool, poolInflationRewardAmount); - IStakingPool(pool).unlockStake(_jobId, _operator, poolFeeRewardAmount, _inflationRewardAmount); + IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, _inflationRewardAmount); } // TODO: emit event } From d3d7f24ea3d712565d570723b774c486359eb0b3 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 16:41:52 +0900 Subject: [PATCH 119/158] refactor code --- .../staking/l2_contracts/SymbioticStaking.sol | 190 ++++++++++-------- 1 file changed, 103 insertions(+), 87 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 8cfb2c7..c9228de 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -12,7 +12,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; -import {Struct} from "../../interfaces/staking/lib/Struct.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -23,8 +23,8 @@ contract SymbioticStaking is ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, - ReentrancyGuardUpgradeable, - ISymbioticStaking + ReentrancyGuardUpgradeable + // ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -37,8 +37,8 @@ contract SymbioticStaking is bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000011; - bytes32 public constant STAKE_SNAPSHOT = keccak256("STAKE_SNAPSHOT"); - bytes32 public constant SLASH_RESULT = keccak256("SLASH_RESULT"); + bytes32 public constant STAKE_SNAPSHOT_TYPE = keccak256("STAKE_SNAPSHOT"); + bytes32 public constant SLASH_RESULT_TYPE = keccak256("SLASH_RESULT"); EnumerableSet.AddressSet stakeTokenSet; @@ -53,8 +53,11 @@ contract SymbioticStaking is /* Symbiotic Snapshot */ - mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; // to check if all partial txs are received - mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // to check if all partial txs are received + + // to track if all partial txs are received + mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; + // to track if all partial txs are received + mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // staked amount for each operator mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; // staked amount for each vault @@ -98,27 +101,24 @@ contract SymbioticStaking is ) external { _checkTransmitterRegistration(_captureTimestamp); - _checkValidity(_index, _numOfTxs, _captureTimestamp, STAKE_SNAPSHOT); + _checkValidity(_index, _numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); // main update logic Struct.VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (Struct.VaultSnapshot[])); - _updateSnapshotInfo(_captureTimestamp, _vaultSnapshots); + _submitVaultSnapshot(_captureTimestamp, _vaultSnapshots); - _updateTxCountInfo(_numOfTxs, _captureTimestamp, STAKE_SNAPSHOT); + _updateTxCountInfo(_numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT]; + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; // when all chunks of OperatorSnapshot are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; } - - if (_isCompleteStatus(_captureTimestamp)) { - _completeSubmission(_captureTimestamp); - } } + // TODO function submitSlashResult( uint256 _index, uint256 _numOfTxs, // number of total transactions @@ -126,27 +126,28 @@ contract SymbioticStaking is bytes memory _SlashResultData, bytes memory _signature ) external { + // Vault Snapshot should be submitted before Slash Result + require(submissionStatus[_captureTimestamp][msg.sender] & STAKE_SNAPSHOT_MASK == STAKE_SNAPSHOT_MASK, "Vault Snapshot not submitted"); + _checkTransmitterRegistration(_captureTimestamp); - _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT); + _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT_TYPE); _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultData, _signature); Struct.JobSlashed[] memory _jobSlashed = abi.decode(_SlashResultData, (Struct.JobSlashed[])); // _updateSlashResultDataInfo(_captureTimestamp, _jobSlashed); - _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT); + _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT_TYPE); - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT]; - // when all chunks of OperatorSnapshot are submitted + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; + + // when all chunks of Snapshots are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT; - } - - if (_isCompleteStatus(_captureTimestamp)) { + submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; _completeSubmission(_captureTimestamp); } - + IStakingManager(stakingManager).onSlashResult(_jobSlashed); // TODO: unlock the selfStake and reward it to the transmitter @@ -185,29 +186,6 @@ contract SymbioticStaking is // TODO: emit event } - function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); - } - - function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { - if(_rewardAmount == 0) return; - - uint256 len = stakeTokenSet.length(); - for(uint256 i = 0; i < len; i++) { - _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization - } - } - - function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { - return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); - } - - function _distributeInflationReward(address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); - } - function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { @@ -223,36 +201,18 @@ contract SymbioticStaking is } } - function _selectLockToken() internal view returns(address) { - require(stakeTokenSet.length() > 0, "No supported token"); + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { + if(_rewardAmount == 0) return; - uint256 idx; - if (stakeTokenSet.length() > 1) { - uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - idx = randomNumber % stakeTokenSet.length(); + uint256 len = stakeTokenSet.length(); + for(uint256 i = 0; i < len; i++) { + _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization } - return stakeTokenSet.at(idx); } - /*======================================== Helpers ========================================*/ - function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { - require(_numOfTxs > 0, "Invalid length"); + /*======================================== internal functions ========================================*/ - // snapshot cannot be submitted before the cooldown period from the last confirmed timestamp (completed snapshot submission) - require(block.timestamp >= (lastConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); - - - Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; - require(_index == snapshot.idxToSubmit, "Invalid index"); - require(_index < snapshot.numOfTxs, "Invalid index"); - require(snapshot.numOfTxs == _numOfTxs, "Invalid numOfTxs"); - - bytes32 mask; - if (_type == STAKE_SNAPSHOT) mask = STAKE_SNAPSHOT_MASK; - else if (_type == SLASH_RESULT) mask = SLASH_RESULT_MASK; - - require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Already submitted"); - } + /*--------------- Snapshot Submission ---------------*/ function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { if(registeredTransmitters[_captureTimestamp] == address(0)) { @@ -266,34 +226,26 @@ contract SymbioticStaking is function _updateTxCountInfo(uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal { Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; - // increase count by 1 - txCountInfo[_captureTimestamp][msg.sender][_type].idxToSubmit += 1; - // update length if 0 if (_snapshot.numOfTxs == 0) { txCountInfo[_captureTimestamp][msg.sender][_type].numOfTxs = _numOfTxs; } - } - function _verifySignature(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes memory _data, bytes memory _signature) internal { - // TODO: Verify the signature - // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image - } - - function _isCompleteStatus(uint256 _captureTimestamp) internal view returns (bool) { - return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; + // increase count by 1 + txCountInfo[_captureTimestamp][msg.sender][_type].idxToSubmit += 1; } - - function _updateSnapshotInfo(uint256 _captureTimestamp, Struct.VaultSnapshot[] memory _vaultSnapshots) internal { + function _submitVaultSnapshot(uint256 _captureTimestamp, Struct.VaultSnapshot[] memory _vaultSnapshots) internal { for (uint256 i = 0; i < _vaultSnapshots.length; i++) { Struct.VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; // update vault staked amount - vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.token] = _vaultSnapshot.stake; + vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.stakeToken] = _vaultSnapshot.stakeAmount; // update operator staked amount - operatorStakedAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.token] += _vaultSnapshot.stake; + operatorStakedAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] += _vaultSnapshot.stakeAmount; + + // TODO: update rewardPerToken in RewardDistributor // TODO: emit event for each update? } @@ -308,6 +260,70 @@ contract SymbioticStaking is // TODO: emit event } + /*--------------- Reward Distribution ---------------*/ + + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); + } + + + function _distributeInflationReward(address _operator, uint256 _amount) internal { + // IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + // IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); + } + + + /*======================================== internal view functions ========================================*/ + + /*--------------- Snapshot Submission ---------------*/ + + function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { + require(_numOfTxs > 0, "Invalid length"); + + // snapshot cannot be submitted before the cooldown period from the last confirmed timestamp (completed snapshot submission) + require(block.timestamp >= (lastConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); + + + Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; + require(_index == snapshot.idxToSubmit, "Invalid index"); + require(_index < snapshot.numOfTxs, "Invalid index"); + require(snapshot.numOfTxs == _numOfTxs, "Invalid numOfTxs"); + + bytes32 mask; + if (_type == STAKE_SNAPSHOT_TYPE) mask = STAKE_SNAPSHOT_MASK; + else if (_type == SLASH_RESULT_TYPE) mask = SLASH_RESULT_MASK; + + require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Already submitted"); + } + + function _verifySignature(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes memory _data, bytes memory _signature) internal { + // TODO: Verify the signature + // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image + } + + function _isCompleteStatus(uint256 _captureTimestamp) internal view returns (bool) { + return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; + } + + /*--------------- Job ---------------*/ + + function _selectLockToken() internal view returns(address) { + require(stakeTokenSet.length() > 0, "No supported token"); + + uint256 idx; + if (stakeTokenSet.length() > 1) { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); + idx = randomNumber % stakeTokenSet.length(); + } + return stakeTokenSet.at(idx); + } + + + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); + } + function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { // TODO: implement logic } From 25a981f178b4701a873a91e25f769fcae06d22e4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 17:26:39 +0900 Subject: [PATCH 120/158] reimplement symbioticStakingReward --- .../l2_contracts/SymbioticStakingReward.sol | 252 ++++++++---------- 1 file changed, 117 insertions(+), 135 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index e89506c..420474e 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -15,6 +15,7 @@ import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol" import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; /* Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, @@ -33,9 +34,11 @@ contract SymbioticStakingReward is using EnumerableSet for EnumerableSet.AddressSet; using Math for uint256; - // TODO: staking token enability should be pulled from SymbioticStaking contract + address public jobManager; + address public stakingPool; - EnumerableSet.AddressSet private _rewardTokenSet; // TODO: remove this, only feeRewardToken and inflationRewardToken will be used + address public feeRewardToken; + address public inflationRewardToken; /* rewardToken: Fee Reward, Inflation Reward @@ -44,24 +47,28 @@ contract SymbioticStakingReward is // total amount staked for each stakeToken // notice: the total amount can be reduced when a job is created and the stake is locked - mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public - totalStakeAmounts; + mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 amount)) public totalStakeAmounts; // locked amount for each stakeToken upon job creation - mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 totalStakeAmount)) public - lockedStakeAmounts; - // reward remaining for each stakeToken - mapping(address rewardToken => mapping(address stakeToken => uint256 amount)) public rewards; - // rewardTokens per stakeToken - mapping(address stakeToken => mapping(address rewardToken => uint256 amount)) public rewardPerTokens; - - // stakeToken supported by each vault should be queried in SymbioticStaking contract - mapping(uint256 captureTimestamp => mapping(address vault => uint256 amount)) public vaultStakeAmounts; - // rewardPerToken to store when update - mapping(uint256 captureTimestamp => mapping(address vault => uint256 rewardPerTokenPaid)) public - vaultRewardPerTokenPaid; + mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 amount)) public lockedStakeAmounts; + + // reward accrued per operator + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount))) rewards; + + // rewardTokens amount per stakeToken + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) + rewardPerTokenStored; + + mapping( + address vault + => mapping( + address stakeToken + => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)) + ) + ) rewardPerTokenPaids; + // reward accrued that the vault can claim - mapping(address vault => uint256 rewardAmount) public claimableRewards; + mapping(address vault => mapping(address rewardToken => uint256 amount)) public rewardAccrued; address public symbioticStaking; @@ -71,7 +78,7 @@ contract SymbioticStakingReward is _; } - //-------------------------------- Init start --------------------------------// + /*============================================= init =============================================*/ // TODO: initialize contract addresses function initialize(address _admin, address _stakingManager) public initializer { __Context_init_unchained(); @@ -85,159 +92,134 @@ contract SymbioticStakingReward is _setStakingManager(_stakingManager); } - //-------------------------------- Init end --------------------------------// - - //-------------------------------- SymbioticStaking start --------------------------------// - /// @notice updates stake amount of a given vault - /// @notice valid only if the captureTimestamp is pushed into confirmedTimestamp in SymbioticStaking contract when submission is completed - /// @dev only can be called by SymbioticStaking contract - function updateVaultStakeAmount(uint256 _captureTimestamp, address _stakeToken, address _vault, uint256 _amount) - external - onlySymbioticStaking - { - require(_captureTimestamp > 0, "zero timestamp"); - require(_stakeToken != address(0) || _vault != address(0), "zero address"); - - // update reward for each rewardToken - uint256 len = _rewardTokenSet.length(); - for (uint256 i = 0; i < len; i++) { - address rewardToken = _rewardTokenSet.at(i); - _update(_captureTimestamp, _vault, _stakeToken, rewardToken); - } - vaultStakeAmounts[_captureTimestamp][_vault] = _amount; - totalStakeAmounts[_captureTimestamp][_stakeToken] += _amount; + /*============================================= external functions =============================================*/ - // TODO: emit event - } - - function claimReward(address _vault) external { - uint256 rewardAmount = claimableRewards[_vault]; - require(rewardAmount > 0, "no reward to claim"); - - claimableRewards[_vault] = 0; + /* ------------------------- reward update ------------------------- */ - // TODO: let the user claim reward of all rewardTokens - - // TODO: emit event + /// @notice called when fee reward is generated + function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlySymbioticStaking { + rewards[_stakeToken][_operator][feeRewardToken] += _rewardAmount; + rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); } - // TODO: decide where to store vault => rewardToken - function _getRewardToken(address vault) internal view returns (address) { - // ISymbioticStaking(symbioticStaking).rewardToken(vault); + /// @notice called when inflation reward is generated + function updateInflationReward(address _operator, uint256 _rewardAmount) external onlySymbioticStaking { + address[] memory stakeTokenLost = _getStakeTokenList(); + for(uint256 i = 0; i < stakeTokenLost.length; i++) { + rewards[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount; + rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); + } } - function addReward(address _stakeToken, address _rewardToken, uint256 _amount) external onlySymbioticStaking { - require(_stakeToken != address(0) || _rewardToken != address(0), "zero address"); - // require(_amount > 0, "zero amount"); - - // IERC20(_rewardToken).safeTransferFrom(_msgSender(), address(this), _amount); - - // update rewardPerToken - uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; + /* ------------------------- symbiotic staking ------------------------- */ - // TODO: emit event + function onSnapshotSubmission(Struct.VaultSnapshot calldata _vaultSnapshots) external onlySymbioticStaking { + // TODO: update rewardPerToken for each stakeToken } + /*============================================= external view functions =============================================*/ + + // TODO: needed? + // function getLatestConfirmedTimestamp() external view returns (uint256) { + // return _latestConfirmedTimestamp(); + // } + + // function getRewardTokens() external view returns (address[] memory) { + // address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); + // uint256 len = _rewardTokenSet.length(); + // for (uint256 i = 0; i < len; i++) { + // _rewardTokens[i] = _rewardTokenSet.at(i); + // } + // return _rewardTokens; + // } - /// @notice rewardToken amount per stakeToken - function _rewardPerToken(address _stakeToken, address _rewardToken) internal view returns (uint256) { - uint256 totalStakeAmount = totalStakeAmountsActive(_stakeToken); - uint256 rewardAmount = rewards[_rewardToken][_stakeToken]; + // /// @notice rewardToken amount per stakeToken + // function _rewardPerToken(address _stakeToken, address _rewardToken, address _operator) + // internal + // view + // returns (uint256) + // { + // uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); + // uint256 rewardAmount = rewards[_rewardToken][_stakeToken]; - return totalStakeAmount == 0 - ? rewardPerTokens[_stakeToken][_rewardToken] - : rewardPerTokens[_stakeToken][_rewardToken] + rewardAmount.mulDiv(1e18, totalStakeAmount); - } + // return operatorStakeAmount == 0 + // ? rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + // : rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + // + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); + // } - function _latestConfirmedTimestamp() internal view returns (uint256) { - return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); - } + // function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { + // return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); + // } - //-------------------------------- SymbioticStaking end --------------------------------// + // function _latestConfirmedTimestamp() internal view returns (uint256) { + // return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); + // } - //-------------------------------- Update start --------------------------------// + /*============================================= internal functions =============================================*/ - function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { - // update rewardPerToken - uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; + /*============================================= internal view functions =============================================*/ - // update reward for each vault - claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); - vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; + function _getStakeTokenList() internal view returns(address[] memory) { + return ISymbioticStaking(symbioticStaking).getStakeTokenList(); } - function _updateRewardPerTokens(address _stakeToken) internal { - uint256 len = _rewardTokenSet.length(); - for(uint256 i = 0; i < len; i++) { - address rewardToken = _rewardTokenSet.at(i); - rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); - } + function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); } - function _pendingReward(address _vault, address _stakeToken, address _rewardToken) - internal - view - returns (uint256) - { - uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); - uint256 rewardPerTokenPaid = vaultRewardPerTokenPaid[latestConfirmedTimestamp][_vault]; - uint256 rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + /*============================================= internal pure functions =============================================*/ - return (vaultStakeAmounts[latestConfirmedTimestamp][_vault].mulDiv((rewardPerToken - rewardPerTokenPaid), 1e18)); - } - //-------------------------------- Update end --------------------------------// - //-------------------------------- Admin start --------------------------------// - function setStakingManager(address _stakingManager) public onlyRole(DEFAULT_ADMIN_ROLE) { - _setStakingManager(_stakingManager); - } + // function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { + // // update rewardPerToken + // uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + // rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; - function _setStakingManager(address _stakingManager) internal { - symbioticStaking = _stakingManager; - // TODO: emit event - } + // // update reward for each vault + // claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); + // vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; + // } - function addRewardToken(address _rewardToken) external onlyRole(DEFAULT_ADMIN_ROLE) { - _rewardTokenSet.add(_rewardToken); - } + // function _updateRewardPerTokens(address _stakeToken) internal { + // uint256 len = _rewardTokenSet.length(); + // for (uint256 i = 0; i < len; i++) { + // address rewardToken = _rewardTokenSet.at(i); + // rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); + // } + // } - function removeRewardToken(address _rewardToken) external onlyRole(DEFAULT_ADMIN_ROLE) { - _rewardTokenSet.remove(_rewardToken); - } + // function _pendingReward(address _vault, address _stakeToken, address _rewardToken) + // internal + // view + // returns (uint256) + // { + // uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); + // uint256 rewardPerTokenPaid = vaultRewardPerTokenPaid[latestConfirmedTimestamp][_vault]; + // uint256 rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + // return (vaultStakeAmounts[latestConfirmedTimestamp][_vault].mulDiv((rewardPerToken - rewardPerTokenPaid), 1e18)); + // } + /*======================================== internal functions ========================================*/ - //-------------------------------- Admin end --------------------------------// + /*======================================== internal view functions ========================================*/ - //-------------------------------- Getter start --------------------------------// - // TODO: needed? - function getLatestConfirmedTimestamp() external view returns (uint256) { - return _latestConfirmedTimestamp(); - } + /*======================================== admin functions ========================================*/ - function getRewardTokens() external view returns (address[] memory) { - address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); - uint256 len = _rewardTokenSet.length(); - for (uint256 i = 0; i < len; i++) { - _rewardTokens[i] = _rewardTokenSet.at(i); - } - return _rewardTokens; + function setStakingManager(address _stakingManager) public onlyRole(DEFAULT_ADMIN_ROLE) { + _setStakingManager(_stakingManager); } - function totalStakeAmountsActive(address _stakeToken) public view returns (uint256) { - uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); - return totalStakeAmounts[latestConfirmedTimestamp][_stakeToken] - - lockedStakeAmounts[latestConfirmedTimestamp][_stakeToken]; + function _setStakingManager(address _stakingManager) internal { + symbioticStaking = _stakingManager; + // TODO: emit event } - //-------------------------------- Getter end --------------------------------// - - //-------------------------------- Overrides start --------------------------------// + /*======================================== overrides ========================================*/ function supportsInterface(bytes4 interfaceId) public @@ -252,4 +234,4 @@ contract SymbioticStakingReward is function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} //-------------------------------- Overrides end --------------------------------// -} \ No newline at end of file +} From 2696f61abe584d9b242769269f87a421ff4a2ae4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 18:23:55 +0900 Subject: [PATCH 121/158] fix symbiotic reward logic --- .../interfaces/staking/IRewardDistributor.sol | 4 +- contracts/interfaces/staking/IStakingPool.sol | 2 + .../staking/ISymbioticStakingReward.sol | 17 ++ .../l2_contracts/SymbioticStakingReward.sol | 198 +++++++++--------- 4 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 contracts/interfaces/staking/ISymbioticStakingReward.sol diff --git a/contracts/interfaces/staking/IRewardDistributor.sol b/contracts/interfaces/staking/IRewardDistributor.sol index ae89780..ce251d6 100644 --- a/contracts/interfaces/staking/IRewardDistributor.sol +++ b/contracts/interfaces/staking/IRewardDistributor.sol @@ -5,11 +5,13 @@ pragma solidity ^0.8.26; interface IRewardDistributor { function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external; - function addInflationReward(address _operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external; + function addInflationReward(address operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external; function onStakeUpdate(address _account, address _stakeToken, address _operator) external; function onClaimReward(address _account, address _operator) external; function onSlash() external; + + function setStakeToken(address _stakingPool, bool _isSupported) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index fc1af14..04ebc0e 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -16,6 +16,8 @@ interface IStakingPool { function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256); + function getVaultStakeAmount(address _vault, address _token) external view returns (uint256); + function rewardDistributor() external view returns (address); function distributeInflationReward(address _operator, uint256 _rewardAmount) external; // Staking Manager only diff --git a/contracts/interfaces/staking/ISymbioticStakingReward.sol b/contracts/interfaces/staking/ISymbioticStakingReward.sol new file mode 100644 index 0000000..083318f --- /dev/null +++ b/contracts/interfaces/staking/ISymbioticStakingReward.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +interface ISymbioticStakingReward { + function updateFeeReward(address _stakeToken, address _operator, uint256 _amount) external; + + function updateInflationReward(address _operator, uint256 _rewardAmount) external; + + // function onStakeUpdate(address _account, address _stakeToken, address _operator) external; + + // function onClaimReward(address _account, address _operator) external; + + // function onSlash() external; + + // function setStakeToken(address _stakingPool, bool _isSupported) external; +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 420474e..b16876b 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -12,6 +12,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -35,7 +36,7 @@ contract SymbioticStakingReward is using Math for uint256; address public jobManager; - address public stakingPool; + address public symbioticStaking; address public feeRewardToken; address public inflationRewardToken; @@ -45,15 +46,8 @@ contract SymbioticStakingReward is stakeToken: staking token */ - // total amount staked for each stakeToken - // notice: the total amount can be reduced when a job is created and the stake is locked - mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 amount)) public totalStakeAmounts; - - // locked amount for each stakeToken upon job creation - mapping(uint256 captureTimestamp => mapping(address stakeToken => uint256 amount)) public lockedStakeAmounts; - // reward accrued per operator - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount))) rewards; + mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount))) rewards; // TODO: check if needed // rewardTokens amount per stakeToken mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) @@ -70,17 +64,20 @@ contract SymbioticStakingReward is // reward accrued that the vault can claim mapping(address vault => mapping(address rewardToken => uint256 amount)) public rewardAccrued; - address public symbioticStaking; - // TODO: vault => claimAddress modifier onlySymbioticStaking() { require(_msgSender() == symbioticStaking, "Caller is not the staking manager"); _; } /*============================================= init =============================================*/ - // TODO: initialize contract addresses - function initialize(address _admin, address _stakingManager) public initializer { + function initialize( + address _admin, + address _jobManager, + address _symbioticStaking, + address _feeRewardToken, + address _inflationRewardToken + ) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -90,7 +87,11 @@ contract SymbioticStakingReward is _grantRole(DEFAULT_ADMIN_ROLE, _admin); - _setStakingManager(_stakingManager); + jobManager = _jobManager; + symbioticStaking = _symbioticStaking; + + feeRewardToken = _feeRewardToken; + inflationRewardToken = _inflationRewardToken; } /*============================================= external functions =============================================*/ @@ -98,125 +99,118 @@ contract SymbioticStakingReward is /* ------------------------- reward update ------------------------- */ /// @notice called when fee reward is generated - function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlySymbioticStaking { - rewards[_stakeToken][_operator][feeRewardToken] += _rewardAmount; - rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); + /// @dev triggered from JobManager when job is completed + function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) + external + onlySymbioticStaking + { + rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += + _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); } /// @notice called when inflation reward is generated function updateInflationReward(address _operator, uint256 _rewardAmount) external onlySymbioticStaking { address[] memory stakeTokenLost = _getStakeTokenList(); - for(uint256 i = 0; i < stakeTokenLost.length; i++) { - rewards[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount; - rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); + for (uint256 i = 0; i < stakeTokenLost.length; i++) { + rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += + _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); } } /* ------------------------- symbiotic staking ------------------------- */ - function onSnapshotSubmission(Struct.VaultSnapshot calldata _vaultSnapshots) external onlySymbioticStaking { - // TODO: update rewardPerToken for each stakeToken + /// @notice update rewardPerToken and rewardAccrued for each vault + /// @dev called when snapshot is submitted + function onSnapshotSubmission(Struct.VaultSnapshot[] calldata _vaultSnapshots) external onlySymbioticStaking { + // TODO: this can be called redundantly as each snapshots can have same operator address + // update inlfationReward info of each vault + address[] memory stakeTokenList = _getStakeTokenList(); + + for (uint256 i = 0; i < _vaultSnapshots.length; i++) { + _updatePendingInflationReward(_vaultSnapshots[i].operator); + + _updateVaultInflationReward(stakeTokenList, _vaultSnapshots[i].vault, _vaultSnapshots[i].operator); + } } - /*============================================= external view functions =============================================*/ - - // TODO: needed? - // function getLatestConfirmedTimestamp() external view returns (uint256) { - // return _latestConfirmedTimestamp(); - // } - - // function getRewardTokens() external view returns (address[] memory) { - // address[] memory _rewardTokens = new address[](_rewardTokenSet.length()); - // uint256 len = _rewardTokenSet.length(); - // for (uint256 i = 0; i < len; i++) { - // _rewardTokens[i] = _rewardTokenSet.at(i); - // } - // return _rewardTokens; - // } - - // /// @notice rewardToken amount per stakeToken - // function _rewardPerToken(address _stakeToken, address _rewardToken, address _operator) - // internal - // view - // returns (uint256) - // { - // uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); - // uint256 rewardAmount = rewards[_rewardToken][_stakeToken]; - - // return operatorStakeAmount == 0 - // ? rewardPerTokenStored[_stakeToken][_operator][_rewardToken] - // : rewardPerTokenStored[_stakeToken][_operator][_rewardToken] - // + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); - // } - - // function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { - // return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); - // } - - // function _latestConfirmedTimestamp() internal view returns (uint256) { - // return ISymbioticStaking(symbioticStaking).lastConfirmedTimestamp(); - // } + /* ------------------------- reward claim ------------------------- */ - /*============================================= internal functions =============================================*/ + /// @notice vault can claim reward calling this function + function claimReward(address _operator) external nonReentrant { + _updatePendingInflationReward(_operator); - /*============================================= internal view functions =============================================*/ + _updateVaultInflationReward(_getStakeTokenList(), _msgSender(), _operator); - function _getStakeTokenList() internal view returns(address[] memory) { - return ISymbioticStaking(symbioticStaking).getStakeTokenList(); - } + // TODO: check transfer logic + IERC20(feeRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][feeRewardToken]); + rewardAccrued[_msgSender()][feeRewardToken] = 0; - function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { - return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); + IERC20(inflationRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][inflationRewardToken]); + rewardAccrued[_msgSender()][inflationRewardToken] = 0; } - /*============================================= internal pure functions =============================================*/ - + /*============================================= external view functions =============================================*/ + function getVaultRewardAccrued(address _vault) external view returns (uint256 feeReward, uint256 inflationReward) { + // TODO: this does not include pending inflation reward as it requires states update in JobManager + return (rewardAccrued[_vault][feeRewardToken], rewardAccrued[_vault][inflationRewardToken]); + } - // function _update(uint256 _captureTimestamp, address _vault, address _stakeToken, address _rewardToken) internal { - // // update rewardPerToken - // uint256 currentRewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); - // rewardPerTokens[_stakeToken][_rewardToken] = currentRewardPerToken; + /*============================================= internal functions =============================================*/ - // // update reward for each vault - // claimableRewards[_vault] += _pendingReward(_vault, _stakeToken, _rewardToken); - // vaultRewardPerTokenPaid[_captureTimestamp][_vault] = currentRewardPerToken; - // } + /// @dev update pending inflation reward and update rewardPerTokenStored of the operator + function _updatePendingInflationReward(address _operator) internal { + IJobManager(jobManager).updateInflationReward(_operator); + } - // function _updateRewardPerTokens(address _stakeToken) internal { - // uint256 len = _rewardTokenSet.length(); - // for (uint256 i = 0; i < len; i++) { - // address rewardToken = _rewardTokenSet.at(i); - // rewardPerTokens[_stakeToken][rewardToken] = _rewardPerToken(_stakeToken, rewardToken); - // } - // } + /// @dev update rewardPerToken and rewardAccrued for each vault + function _updateVaultInflationReward(address[] memory _stakeTokenList, address _vault, address _operator) + internal + { + for (uint256 i = 0; i < _stakeTokenList.length; i++) { + address stakeToken = _stakeTokenList[i]; + uint256 operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][_operator][inflationRewardToken]; + uint256 vaultRewardPerTokenPaid = rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken]; + + rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(_vault, stakeToken).mulDiv( + operatorRewardPerTokenStored - vaultRewardPerTokenPaid, 1e18 + ); + rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken] = operatorRewardPerTokenStored; + } + } - // function _pendingReward(address _vault, address _stakeToken, address _rewardToken) - // internal - // view - // returns (uint256) - // { - // uint256 latestConfirmedTimestamp = _latestConfirmedTimestamp(); - // uint256 rewardPerTokenPaid = vaultRewardPerTokenPaid[latestConfirmedTimestamp][_vault]; - // uint256 rewardPerToken = _rewardPerToken(_stakeToken, _rewardToken); + /*============================================= internal view functions =============================================*/ - // return (vaultStakeAmounts[latestConfirmedTimestamp][_vault].mulDiv((rewardPerToken - rewardPerTokenPaid), 1e18)); - // } + function _getStakeTokenList() internal view returns (address[] memory) { + return ISymbioticStaking(symbioticStaking).getStakeTokenList(); + } - /*======================================== internal functions ========================================*/ + function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); + } - /*======================================== internal view functions ========================================*/ + function _getVaultStakeAmount(address _vault, address _stakeToken) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getVaultStakeAmount(_vault, _stakeToken); + } + /*============================================= internal pure functions =============================================*/ /*======================================== admin functions ========================================*/ - function setStakingManager(address _stakingManager) public onlyRole(DEFAULT_ADMIN_ROLE) { - _setStakingManager(_stakingManager); + function setStakingPool(address _symbioticStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { + symbioticStaking = _symbioticStaking; } - function _setStakingManager(address _stakingManager) internal { - symbioticStaking = _stakingManager; - // TODO: emit event + function setJobManager(address _jobManager) public onlyRole(DEFAULT_ADMIN_ROLE) { + jobManager = _jobManager; + } + + function setFeeRewardToken(address _feeRewardToken) public onlyRole(DEFAULT_ADMIN_ROLE) { + feeRewardToken = _feeRewardToken; + } + + function setInflationRewardToken(address _inflationRewardToken) public onlyRole(DEFAULT_ADMIN_ROLE) { + inflationRewardToken = _inflationRewardToken; } /*======================================== overrides ========================================*/ @@ -232,6 +226,4 @@ contract SymbioticStakingReward is } function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} - - //-------------------------------- Overrides end --------------------------------// } From 06276b8b86f3d1eb62809158d0d2b161de4d80f9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 19:42:57 +0900 Subject: [PATCH 122/158] fix compile errors --- contracts/interfaces/staking/IStakingPool.sol | 2 - contracts/lib/staking/Struct.sol | 17 +++- .../staking/l2_contracts/SymbioticStaking.sol | 79 +++++++++++-------- .../l2_contracts/SymbioticStakingReward.sol | 6 +- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index 04ebc0e..fc1af14 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -16,8 +16,6 @@ interface IStakingPool { function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256); - function getVaultStakeAmount(address _vault, address _token) external view returns (uint256); - function rewardDistributor() external view returns (address); function distributeInflationReward(address _operator, uint256 _rewardAmount) external; // Staking Manager only diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 50fcfa0..452acc0 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -26,7 +26,7 @@ library Struct { struct JobSlashed { uint256 jobId; - address operator; + address operator; // TODO: check if cheaper than pulling from JobManager address rewardAddress; } @@ -35,11 +35,14 @@ library Struct { uint256 numOfTxs; // total number of txs for the snapshot } + /*==================== Symbiotic Staking ==================== */ + + /* Snapshot Submission */ struct VaultSnapshot { address operator; address vault; - address token; - uint256 stake; + address stakeToken; + uint256 stakeAmount; } struct ConfirmedTimestamp { @@ -48,12 +51,20 @@ library Struct { uint256 transmitterComissionRate; } + // struct OperatorSnapshot { + // address operator; + // address[] stakeTokens; + // uint256[] stakeAmounts; + // } + + /* Job Lock */ struct SymbioticStakingLock { address stakeToken; uint256 amount; // transmitter who submitted with confirmedTimestamp used when job is created address transmitter; } + struct PoolConfig { uint256 weight; bool enabled; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index c9228de..590c36e 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -11,6 +11,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {ISymbioticStakingReward} from "../../interfaces/staking/ISymbioticStakingReward.sol"; import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -23,8 +24,8 @@ contract SymbioticStaking is ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, - ReentrancyGuardUpgradeable - // ISymbioticStaking + ReentrancyGuardUpgradeable, + ISymbioticStaking { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -59,16 +60,16 @@ contract SymbioticStaking is // to track if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // staked amount for each operator - mapping(uint256 captureTimestamp => mapping(address operator => mapping(address token => uint256 stakeAmount))) operatorStakedAmounts; + mapping(uint256 captureTimestamp => mapping(address operator => mapping(address stakeToken => uint256 stakeAmount))) operatorStakeAmounts; // staked amount for each vault - mapping(uint256 captureTimestamp => mapping(address vault => mapping(address token => uint256 stakeAmount))) vaultStakedAmounts; + mapping(uint256 captureTimestamp => mapping(address vault => mapping(address operator => mapping(address stakeToken => uint256 stakeAmount)))) vaultStakeAmounts; Struct.ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received /* Staking */ mapping(uint256 jobId => Struct.SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake - mapping(address operator => mapping(address token => uint256 locked)) public operatorLockedAmounts; + mapping(address operator => mapping(address stakeToken => uint256 locked)) public operatorLockedAmounts; mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; // only one transmitter can submit the snapshot for the same capturetimestamp @@ -90,7 +91,9 @@ contract SymbioticStaking is rewardDistributor = _rewardDistributor; } - /*======================================== L1 to L2 Transmission ========================================*/ + /*============================================= external functions =============================================*/ + + /* ------------------------- L1 to L2 submission ------------------------- */ function submitVaultSnapshot( uint256 _index, @@ -153,7 +156,8 @@ contract SymbioticStaking is // TODO: unlock the selfStake and reward it to the transmitter } - /*======================================== Job ========================================*/ + /* ------------------------- stake lock/unlock for job ------------------------- */ + function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); require(getOperatorActiveStakeAmount(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); @@ -166,7 +170,7 @@ contract SymbioticStaking is // TODO: emit event } - function unlockStake(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; uint256 transmitterComissionRate = confirmedTimestamps[confirmedTimestamps.length - 1].transmitterComissionRate; uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); @@ -186,6 +190,8 @@ contract SymbioticStaking is // TODO: emit event } + /* ------------------------- slash ------------------------- */ + function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { @@ -240,10 +246,10 @@ contract SymbioticStaking is Struct.VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; // update vault staked amount - vaultStakedAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.stakeToken] = _vaultSnapshot.stakeAmount; + vaultStakeAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] = _vaultSnapshot.stakeAmount; // update operator staked amount - operatorStakedAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] += _vaultSnapshot.stakeAmount; + operatorStakeAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] += _vaultSnapshot.stakeAmount; // TODO: update rewardPerToken in RewardDistributor @@ -264,15 +270,42 @@ contract SymbioticStaking is function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); + ISymbioticStakingReward(rewardDistributor).updateFeeReward(_stakeToken, _operator, _amount); } function _distributeInflationReward(address _operator, uint256 _amount) internal { - // IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - // IRewardDistributor(rewardDistributor).addInflationReward(_operator, _amount); + IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + ISymbioticStakingReward(rewardDistributor).updateInflationReward(_operator, _amount); } + /*======================================== external view functions ========================================*/ + + function lastConfirmedTimestamp() public view returns (uint256) { + return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; + } + + function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorStakeAmounts[lastConfirmedTimestamp()][_operator][_token]; + } + + function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { + uint256 operatorStakeAmount = operatorStakeAmounts[lastConfirmedTimestamp()][_operator][_token]; + uint256 operatorLockedAmount = operatorLockedAmounts[_operator][_token]; + return operatorStakeAmount > operatorLockedAmount ? operatorStakeAmount - operatorLockedAmount : 0; + } + + function getStakeAmount(address _vault, address _stakeToken, address _operator) external view returns (uint256) { + return vaultStakeAmounts[lastConfirmedTimestamp()][_vault][_stakeToken][_operator]; + } + + function getStakeTokenList() external view returns(address[] memory) { + return stakeTokenSet.values(); + } + + function isSupportedToken(address _token) public view returns (bool) { + return stakeTokenSet.contains(_token); + } /*======================================== internal view functions ========================================*/ @@ -328,26 +361,6 @@ contract SymbioticStaking is // TODO: implement logic } - /*======================================== Getters ========================================*/ - - function lastConfirmedTimestamp() public view returns (uint256) { - return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; - } - - function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; - } - - function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - uint256 operatorStakeAmount = operatorStakedAmounts[lastConfirmedTimestamp()][_operator][_token]; - uint256 operatorLockedAmount = operatorLockedAmounts[_operator][_token]; - return operatorStakeAmount > operatorLockedAmount ? operatorStakeAmount - operatorLockedAmount : 0; - } - - function isSupportedToken(address _token) public view returns (bool) { - return stakeTokenSet.contains(_token); - } - /*======================================== Admin ========================================*/ function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingManager = _stakingManager; diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index b16876b..6180172 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -172,7 +172,7 @@ contract SymbioticStakingReward is uint256 operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][_operator][inflationRewardToken]; uint256 vaultRewardPerTokenPaid = rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken]; - rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(_vault, stakeToken).mulDiv( + rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(_vault, stakeToken, _operator).mulDiv( operatorRewardPerTokenStored - vaultRewardPerTokenPaid, 1e18 ); rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken] = operatorRewardPerTokenStored; @@ -189,8 +189,8 @@ contract SymbioticStakingReward is return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); } - function _getVaultStakeAmount(address _vault, address _stakeToken) internal view returns (uint256) { - return ISymbioticStaking(symbioticStaking).getVaultStakeAmount(_vault, _stakeToken); + function _getVaultStakeAmount(address _vault, address _stakeToken, address _operator) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getStakeAmount(_vault, _stakeToken, _operator); } /*============================================= internal pure functions =============================================*/ From 9339d7d3b0ed8da8732bf4957623feca912a2e8f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 20:25:28 +0900 Subject: [PATCH 123/158] fix transmitter comission rate logic --- .../staking/ISymbioticStakingReward.sol | 10 +++----- contracts/lib/staking/Struct.sol | 7 +----- .../staking/l2_contracts/SymbioticStaking.sol | 25 ++++++++++++++----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/contracts/interfaces/staking/ISymbioticStakingReward.sol b/contracts/interfaces/staking/ISymbioticStakingReward.sol index 083318f..b7fc905 100644 --- a/contracts/interfaces/staking/ISymbioticStakingReward.sol +++ b/contracts/interfaces/staking/ISymbioticStakingReward.sol @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MIT +import {Struct} from "../../lib/staking/Struct.sol"; + pragma solidity ^0.8.26; interface ISymbioticStakingReward { @@ -7,11 +9,5 @@ interface ISymbioticStakingReward { function updateInflationReward(address _operator, uint256 _rewardAmount) external; - // function onStakeUpdate(address _account, address _stakeToken, address _operator) external; - - // function onClaimReward(address _account, address _operator) external; - - // function onSlash() external; - - // function setStakeToken(address _stakingPool, bool _isSupported) external; + function onSnapshotSubmission(Struct.VaultSnapshot[] calldata _vaultSnapshots) external; } \ No newline at end of file diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 452acc0..7d32d49 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -51,18 +51,13 @@ library Struct { uint256 transmitterComissionRate; } - // struct OperatorSnapshot { - // address operator; - // address[] stakeTokens; - // uint256[] stakeAmounts; - // } - /* Job Lock */ struct SymbioticStakingLock { address stakeToken; uint256 amount; // transmitter who submitted with confirmedTimestamp used when job is created address transmitter; + uint256 transmitterComissionRate; } struct PoolConfig { diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 590c36e..c709073 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -163,8 +163,10 @@ contract SymbioticStaking is require(getOperatorActiveStakeAmount(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); // Store transmitter address to reward when job is closed - address transmitter = confirmedTimestamps[confirmedTimestamps.length - 1].transmitter; - lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], transmitter); + uint256 timestampIdx = confirmedTimestamps.length - 1; + address transmitter = confirmedTimestamps[timestampIdx].transmitter; + + lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], transmitter, confirmedTimestamps[timestampIdx].transmitterComissionRate); operatorLockedAmounts[_operator][_token] += amountToLock[_token]; // TODO: emit event @@ -172,18 +174,25 @@ contract SymbioticStaking is function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; - uint256 transmitterComissionRate = confirmedTimestamps[confirmedTimestamps.length - 1].transmitterComissionRate; + + uint256 transmitterComissionRate = lock.transmitterComissionRate; uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); + uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; + // distribute fee reward if(feeRewardRemaining > 0) { _distributeFeeReward(lock.stakeToken, _operator, feeRewardRemaining); } + // distribute inflation reward if(_inflationRewardAmount > 0) { _distributeInflationReward(_operator, _inflationRewardAmount); } + // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation + IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); + delete lockInfo[_jobId]; operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; @@ -251,14 +260,14 @@ contract SymbioticStaking is // update operator staked amount operatorStakeAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] += _vaultSnapshot.stakeAmount; - // TODO: update rewardPerToken in RewardDistributor - // TODO: emit event for each update? } + + ISymbioticStakingReward(rewardDistributor).onSnapshotSubmission(_vaultSnapshots); } function _completeSubmission(uint256 _captureTimestamp) internal { - uint256 transmitterComission = _transmitterComissionRate(lastConfirmedTimestamp()); + uint256 transmitterComission = _calcTransmitterComissionRate(_captureTimestamp); Struct.ConfirmedTimestamp memory confirmedTimestamp = Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); confirmedTimestamps.push(confirmedTimestamp); @@ -339,6 +348,10 @@ contract SymbioticStaking is return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; } + function _calcTransmitterComissionRate(uint256 _confirmedTimestamp) internal view returns(uint256) { + // TODO: (block.timestamp - _lastConfirmedTimestamp) * X + } + /*--------------- Job ---------------*/ function _selectLockToken() internal view returns(address) { From a1798aa2ac175ad0f6df02194d1339cb51b97d71 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 23:17:46 +0900 Subject: [PATCH 124/158] add comments --- .../staking/l2_contracts/SymbioticStaking.sol | 34 +++++++++++-------- .../l2_contracts/SymbioticStakingReward.sol | 11 ++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index c709073..b2686c8 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -93,7 +93,7 @@ contract SymbioticStaking is /*============================================= external functions =============================================*/ - /* ------------------------- L1 to L2 submission ------------------------- */ + /*------------------------- L1 to L2 submission ------------------------- */ function submitVaultSnapshot( uint256 _index, @@ -107,15 +107,18 @@ contract SymbioticStaking is _checkValidity(_index, _numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); - - // main update logic + Struct.VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (Struct.VaultSnapshot[])); + + // update Vault and Operator stake amount + // update rewardPerToken for each vault and operator in SymbioticStakingReward _submitVaultSnapshot(_captureTimestamp, _vaultSnapshots); _updateTxCountInfo(_numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; - // when all chunks of OperatorSnapshot are submitted + + // when all chunks of VaultSnapshots are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; } @@ -151,12 +154,13 @@ contract SymbioticStaking is _completeSubmission(_captureTimestamp); } + // IStakingManager(stakingManager).onSlashResult(_jobSlashed); // TODO: unlock the selfStake and reward it to the transmitter } - /* ------------------------- stake lock/unlock for job ------------------------- */ + /*------------------------- stake lock/unlock for job -------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); @@ -193,13 +197,14 @@ contract SymbioticStaking is // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); + // unlock the stake locked during job creation delete lockInfo[_jobId]; operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; // TODO: emit event } - /* ------------------------- slash ------------------------- */ + /*------------------------- slash -------------------------*/ function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; @@ -207,8 +212,8 @@ contract SymbioticStaking is Struct.SymbioticStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; uint256 lockedAmount = lock.amount; - if(lockedAmount == 0) continue; + // unlock the stake locked during job creation operatorLockedAmounts[_slashedJobs[i].operator][lock.stakeToken] -= lockedAmount; delete lockInfo[_slashedJobs[i].jobId]; @@ -227,7 +232,7 @@ contract SymbioticStaking is /*======================================== internal functions ========================================*/ - /*--------------- Snapshot Submission ---------------*/ + /*------------------------- Snapshot Submission -------------------------*/ function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { if(registeredTransmitters[_captureTimestamp] == address(0)) { @@ -275,7 +280,7 @@ contract SymbioticStaking is // TODO: emit event } - /*--------------- Reward Distribution ---------------*/ + /*------------------------- Reward Distribution -------------------------*/ function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); @@ -352,7 +357,7 @@ contract SymbioticStaking is // TODO: (block.timestamp - _lastConfirmedTimestamp) * X } - /*--------------- Job ---------------*/ + /*------------------------- Job -------------------------*/ function _selectLockToken() internal view returns(address) { require(stakeTokenSet.length() > 0, "No supported token"); @@ -365,15 +370,16 @@ contract SymbioticStaking is return stakeTokenSet.at(idx); } + function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { + // TODO: implement logic + } + + /*------------------------- Reward -------------------------*/ function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } - function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { - // TODO: implement logic - } - /*======================================== Admin ========================================*/ function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingManager = _stakingManager; diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 6180172..d5d61bc 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -126,9 +126,13 @@ contract SymbioticStakingReward is // update inlfationReward info of each vault address[] memory stakeTokenList = _getStakeTokenList(); + // update pending inflation reward and update rewardPerToken for each operator and vault + // feeReward is updated on job completion so no need to update here for (uint256 i = 0; i < _vaultSnapshots.length; i++) { + // update pending inflation reward of the operator and update rewardPerTokenStored _updatePendingInflationReward(_vaultSnapshots[i].operator); + // update rewardPerTokenPaid and rewardAccrued for each vault _updateVaultInflationReward(stakeTokenList, _vaultSnapshots[i].vault, _vaultSnapshots[i].operator); } } @@ -137,14 +141,18 @@ contract SymbioticStakingReward is /// @notice vault can claim reward calling this function function claimReward(address _operator) external nonReentrant { + // update pending inflation reward of the operator and update rewardPerTokenStored _updatePendingInflationReward(_operator); + // update rewardPerTokenPaid and rewardAccrued for each vault _updateVaultInflationReward(_getStakeTokenList(), _msgSender(), _operator); // TODO: check transfer logic + // transfer fee reward to the vault IERC20(feeRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][feeRewardToken]); rewardAccrued[_msgSender()][feeRewardToken] = 0; + // transfer inflation reward to the vault IERC20(inflationRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][inflationRewardToken]); rewardAccrued[_msgSender()][inflationRewardToken] = 0; } @@ -172,9 +180,12 @@ contract SymbioticStakingReward is uint256 operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][_operator][inflationRewardToken]; uint256 vaultRewardPerTokenPaid = rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken]; + // update reward accrued for the vault rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(_vault, stakeToken, _operator).mulDiv( operatorRewardPerTokenStored - vaultRewardPerTokenPaid, 1e18 ); + + // update rewardPerTokenPaid of the vault rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken] = operatorRewardPerTokenStored; } } From 18c98a65e20f8d679fa1f5fc1bbf29e58e571063 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 23:18:02 +0900 Subject: [PATCH 125/158] add getter for pendingInflationReward --- contracts/staking/l2_contracts/JobManager.sol | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 4cc4fe0..36722d8 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -106,11 +106,12 @@ contract JobManager is uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); // send fee and unlock stake + // TODO: consider where the fund comes from IERC20(feeToken).safeTransfer(stakingManager, feePaid); // TODO: make RewardDistributor pull fee from JobManager IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); - _updateJobCompletionEpoch(_jobId); + _updateJobCompletionEpoch(_jobId); // TODO } /** @@ -137,7 +138,7 @@ contract JobManager is } } - /*======================================== Fee ========================================*/ + /*======================================== Fee Reward ========================================*/ /// @notice refund fee to the job requester /// @dev most likely called by the requester when job is not completed @@ -171,6 +172,10 @@ contract JobManager is } } + function getPendingInflationReward(address _operator) external view returns(uint256) { + return _getPendingInflationReward(_operator); + } + /*======================================== Internal functions ========================================*/ function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { @@ -188,12 +193,12 @@ contract JobManager is uint256[] storage completedEpochs = operatorJobCompletionEpochs[_operator]; // first epoch which the reward has not been distributed - uint256 _beginIdx = inflationRewardEpochBeginIdx[_operator]; + uint256 beginIdx = inflationRewardEpochBeginIdx[_operator]; // no job completed since last update - if(_beginIdx > completedEpochs.length) return 0; + if(beginIdx > completedEpochs.length) return 0; - uint256 beginEpoch = completedEpochs[_beginIdx]; + uint256 beginEpoch = completedEpochs[beginIdx]; uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; // no pending reward if operator has already claimed reward until latest epoch @@ -203,7 +208,7 @@ contract JobManager is uint256 rewardPerEpoch = inflationRewardPerEpoch; // cache uint256 len = completedEpochs.length; - for(uint256 idx = _beginIdx; idx < len; idx++) { + for(uint256 idx = beginIdx; idx < len; idx++) { uint256 epoch = completedEpochs[idx]; // for last epoch in epoch array @@ -218,6 +223,38 @@ contract JobManager is return pendingInflationReward; } + function _getPendingInflationReward(address _operator) internal view returns(uint256 pendingInflationReward) { + // check if operator has completed any job + if (operatorJobCompletionEpochs[_operator].length == 0) return 0; + + // list of epochs in which operator has completed jobs + uint256[] storage completedEpochs = operatorJobCompletionEpochs[_operator]; + + // first epoch which the reward has not been distributed + uint256 beginIdx = inflationRewardEpochBeginIdx[_operator]; + + // no job completed since last update + if(beginIdx > completedEpochs.length) return 0; + + uint256 beginEpoch = completedEpochs[beginIdx]; + uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; + + // no pending reward if operator has already claimed reward until latest epoch + if(beginEpoch == currentEpoch) return 0; + + // update pending reward + uint256 rewardPerEpoch = inflationRewardPerEpoch; // cache + uint256 len = completedEpochs.length; + + for(uint256 idx = beginIdx; idx < len; idx++) { + uint256 epoch = completedEpochs[idx]; + + pendingInflationReward += Math.mulDiv(rewardPerEpoch, operatorJobCount[epoch][_operator], totalJobCount[epoch]); + } + + return pendingInflationReward; + } + /*======================================== Admin ========================================*/ function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { From c2f1192140d6ea3fb8e3dc502973e9820709754e Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 23:18:13 +0900 Subject: [PATCH 126/158] fix variable name --- contracts/staking/l2_contracts/NativeStaking.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 46fe193..30443a5 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -49,7 +49,7 @@ contract NativeStaking is mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakeAmounts; /* Locked Stakes */ - mapping(uint256 jobId => Struct.NativeStakingLock lock) public jobLockedAmounts; + mapping(uint256 jobId => Struct.NativeStakingLock lock) public lockInfo; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; modifier onlySupportedToken(address _stakeToken) { @@ -180,7 +180,7 @@ contract NativeStaking is require(_getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake - jobLockedAmounts[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); + lockInfo[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); operatorLockedAmounts[_operator][_token] += _amountToLock; // TODO: emit event @@ -189,7 +189,7 @@ contract NativeStaking is /// @notice unlock stake and distribute reward /// @dev called by StakingManager when job is completed function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { - Struct.NativeStakingLock memory lock = jobLockedAmounts[_jobId]; + Struct.NativeStakingLock memory lock = lockInfo[_jobId]; if(lock.amount == 0) return; @@ -210,7 +210,7 @@ contract NativeStaking is function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; for (uint256 i = 0; i < len; i++) { - Struct.NativeStakingLock memory lock = jobLockedAmounts[_slashedJobs[i].jobId]; + Struct.NativeStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; uint256 lockedAmount = lock.amount; if(lockedAmount == 0) continue; // if already slashed @@ -254,7 +254,7 @@ contract NativeStaking is function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { operatorLockedAmounts[_operator][_stakeToken] -= _amount; - delete jobLockedAmounts[_jobId]; + delete lockInfo[_jobId]; } function _selectTokenToLock() internal view returns(address) { From 5b75efa875f21acf90f5e3051309cca1a71d51ad Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 23:18:26 +0900 Subject: [PATCH 127/158] temporarily comment out --- .../l2_contracts/NativeStakingReward.sol | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStakingReward.sol b/contracts/staking/l2_contracts/NativeStakingReward.sol index 5b9ff49..40e5658 100644 --- a/contracts/staking/l2_contracts/NativeStakingReward.sol +++ b/contracts/staking/l2_contracts/NativeStakingReward.sol @@ -1,43 +1,43 @@ -// SPDX-License-Identifier: MIT +// // SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; +// pragma solidity ^0.8.26; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +// import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; -import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; -import {RewardDistributor} from "./RewardDistributor.sol"; +// import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +// import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; +// import {RewardDistributor} from "./RewardDistributor.sol"; -contract NativeStakingReward is - RewardDistributor -{ +// contract NativeStakingReward is +// RewardDistributor +// { - //-------------------------------- Init start --------------------------------// +// //-------------------------------- Init start --------------------------------// - //-------------------------------- Init end --------------------------------// +// //-------------------------------- Init end --------------------------------// - //-------------------------------- Staking start --------------------------------// +// //-------------------------------- Staking start --------------------------------// - function _getUserStakeAmount(address account, address token, address operator) internal view returns (uint256) { - // return INativeStaking(stakingPool).getUserStakeAmount(account, token, operator); - } +// function _getUserStakeAmount(address account, address token, address operator) internal view returns (uint256) { +// // return INativeStaking(stakingPool).getUserStakeAmount(account, token, operator); +// } - //-------------------------------- Staking end --------------------------------// +// //-------------------------------- Staking end --------------------------------// - //-------------------------------- Admin start --------------------------------// +// //-------------------------------- Admin start --------------------------------// - //-------------------------------- Admin end --------------------------------// +// //-------------------------------- Admin end --------------------------------// - //-------------------------------- Overrides start --------------------------------// +// //-------------------------------- Overrides start --------------------------------// - //-------------------------------- Overrides end --------------------------------// +// //-------------------------------- Overrides end --------------------------------// -} +// } From 29fa07df78a112c7d61b11f8277ad6eb9cf4d005 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sat, 5 Oct 2024 23:18:47 +0900 Subject: [PATCH 128/158] temporarily comment out --- .../l2_contracts/RewardDistributor.sol | 125 +++++++++--------- .../staking/l2_contracts/StakingManager.sol | 2 - 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/contracts/staking/l2_contracts/RewardDistributor.sol b/contracts/staking/l2_contracts/RewardDistributor.sol index 3858e14..084b16e 100644 --- a/contracts/staking/l2_contracts/RewardDistributor.sol +++ b/contracts/staking/l2_contracts/RewardDistributor.sol @@ -16,6 +16,8 @@ import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.so import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {Struct} from "../../lib/staking/Struct.sol"; @@ -30,6 +32,7 @@ abstract contract RewardDistributor is { using Math for uint256; using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; address public jobManager; address public stakingPool; @@ -63,6 +66,8 @@ abstract contract RewardDistributor is _; } + /*============================================= init =============================================*/ + function initialize( address _admin, address _jobManager, @@ -90,71 +95,71 @@ abstract contract RewardDistributor is inflationRewardToken = _inflationRewardToken; } - function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external onlyStakingPool { - rewards[_stakeToken][_operator][feeRewardToken] += _amount; + /*======================================== external functions ========================================*/ - rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); + /// @notice called when fee reward is generated + function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlyStakingPool { + rewards[_stakeToken][_operator][feeRewardToken] += _rewardAmount; + rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); } - /// @notice distribute inflation reward to all stakeTokens - /// @dev StakingPool should pass the list of stakeTokens and rewardAmount for each stakeToken - function addInflationReward(address _operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external onlyStakingPool { - address _inflationRewardToken = inflationRewardToken; // cache - for (uint256 i = 0; i < stakeTokens.length; i++) { - // distribute inflation reward to each stakeToken - rewards[stakeTokens[i]][_operator][_inflationRewardToken] += rewardAmounts[i]; - - // update rewardPerTokenStored for each stakeToken - rewardPerTokenStored[stakeTokens[i]][_operator][_inflationRewardToken] = _rewardPerTokenStored(stakeTokens[i], _operator, _inflationRewardToken); + /// @notice called when inflation reward is generated + function updateInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingPool { + address[] memory stakeTokenLost = _getStakeTokenList(); + for(uint256 i = 0; i < stakeTokenLost.length; i++) { + rewards[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount; + rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); } - // TODO: emit event } - /// @dev called when stake amount is updated in StakingPool - function onStakeUpdate(address _account, address _stakeToken, address _operator) external onlyStakingPool { - // update fee reward - rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); + // /// @dev called when stake amount is updated in StakingPool + // function onStakeUpdate(address _account, address _stakeToken, address _operator) external onlyStakingPool { + // // update fee reward + // rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); - // update inflation reward - // TODO: check if there is any problem by not updating rewardPerTokenStored during Tx - _requestInflationRewardUpdate(_operator); + // // update inflation reward + // // TODO: check if there is any problem by not updating rewardPerTokenStored during Tx + // _requestInflationRewardUpdate(_operator); - uint256 rewardPerTokenStoredCurrent = rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken]; - address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); + // uint256 rewardPerTokenStoredCurrent = rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken]; + // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); - for(uint256 i = 0; i < stakeTokenList.length; i++) { - uint256 rewardPerTokenPaid = rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken]; - uint256 accountStakeAmount = _getStakeAmount(_account, stakeTokenList[i], _operator); - uint256 pendingReward = accountStakeAmount.mulDiv(rewardPerTokenStoredCurrent - rewardPerTokenPaid, 1e18); - - // update account's reward info - rewardAccrued[_account][inflationRewardToken] += pendingReward; - rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStoredCurrent; - - // update global rewardPerTokenStored - uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, stakeTokenList[i]); - rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken] += rewards[stakeTokenList[i]][_operator][inflationRewardToken].mulDiv(1e18, operatorStakeAmount); - } - } + // for(uint256 i = 0; i < stakeTokenList.length; i++) { + // uint256 rewardPerTokenPaid = rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken]; + // uint256 accountStakeAmount = _getStakeAmount(_account, stakeTokenList[i], _operator); + // uint256 pendingReward = accountStakeAmount.mulDiv(rewardPerTokenStoredCurrent - rewardPerTokenPaid, 1e18); - function onClaimReward(address _account, address _operator) external onlyStakingPool { - IERC20(feeRewardToken).safeTransfer(_account, rewardAccrued[_account][feeRewardToken]); - IERC20(inflationRewardToken).safeTransfer(_account, rewardAccrued[_account][inflationRewardToken]); + // // update account's reward info + // rewardAccrued[_account][inflationRewardToken] += pendingReward; + // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStoredCurrent; + + // // update global rewardPerTokenStored + // uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, stakeTokenList[i]); + // rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken] += rewards[stakeTokenList[i]][_operator][inflationRewardToken].mulDiv(1e18, operatorStakeAmount); + // } + // } + + // function onClaimReward(address _account, address _operator) external onlyStakingPool { + // IERC20(feeRewardToken).safeTransfer(_account, rewardAccrued[_account][feeRewardToken]); + // IERC20(inflationRewardToken).safeTransfer(_account, rewardAccrued[_account][inflationRewardToken]); + + // rewardAccrued[_account][feeRewardToken] = 0; + // rewardAccrued[_account][inflationRewardToken] = 0; + + // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); + // for(uint256 i = 0; i < stakeTokenList.length; i++) { + // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][feeRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][feeRewardToken]; + // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken]; + // } + // } - rewardAccrued[_account][feeRewardToken] = 0; - rewardAccrued[_account][inflationRewardToken] = 0; - address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); - for(uint256 i = 0; i < stakeTokenList.length; i++) { - rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][feeRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][feeRewardToken]; - rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken]; - } - } function onSlash() external onlyStakingPool { // TODO } + /*======================================== internal functions ========================================*/ function _requestInflationRewardUpdate(address _operator) internal { // JobManager.updateInflationReward // -> StakingManager.distributeInflationReward @@ -163,23 +168,24 @@ abstract contract RewardDistributor is IJobManager(jobManager).updateInflationReward(_operator); } - /* Modification for _update */ - function _rewardPerTokenStored(address _stakeToken, address _operator, address _rewardToken) + function _rewardPerTokenStored(address _stakeToken, address _operator, address _rewardToken, uint256 rewardAmount) internal view returns (uint256) { uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); - uint256 totalRewardAmount = rewards[_stakeToken][_operator][_rewardToken]; + if(operatorStakeAmount == 0) return rewardPerTokenStored[_stakeToken][_operator][_rewardToken]; - // TODO: make sure decimal is 18 - return operatorStakeAmount == 0 - ? rewardPerTokenStored[_stakeToken][_operator][_rewardToken] - : rewardPerTokenStored[_stakeToken][_operator][_rewardToken] - + totalRewardAmount.mulDiv(1e18, operatorStakeAmount); + return rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + rewardAmount.mulDiv(1e18, operatorStakeAmount); } + function _updatePendingInflationReward(address _operator) internal { + + } + + + /*======================================== internal view functions ========================================*/ function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { return IStakingPool(stakingPool).getOperatorStakeAmount(_operator, _stakeToken); } @@ -192,17 +198,15 @@ abstract contract RewardDistributor is return IStakingPool(stakingPool).getStakeAmount(account, _stakeToken, _operator); } - //-------------------------------- Admin start --------------------------------// + /*======================================== admin functions ========================================*/ function setStakingPool(address _stakingPool) public onlyRole(DEFAULT_ADMIN_ROLE) { stakingPool = _stakingPool; - // TODO: emit event } - //-------------------------------- Admin end --------------------------------// - //-------------------------------- Overrides start --------------------------------// + /*======================================== overrides ========================================*/ function supportsInterface(bytes4 interfaceId) public @@ -216,5 +220,4 @@ abstract contract RewardDistributor is function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} - //-------------------------------- Overrides end --------------------------------// } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 63a33cb..d9cb6cc 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -123,8 +123,6 @@ contract StakingManager is uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - if(pool == msg.sender) continue; // skip if called by SymbioticStaking - IStakingPool(pool).slash(_jobsSlashed); } } From a5d9da43986d20ecc8fe20feff2ff791f624d740 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Sun, 6 Oct 2024 00:46:58 +0900 Subject: [PATCH 129/158] add comments --- contracts/staking/l2_contracts/SymbioticStaking.sol | 4 ++-- contracts/staking/l2_contracts/SymbioticStakingReward.sol | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index b2686c8..aed2808 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -148,14 +148,14 @@ contract SymbioticStaking is Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; + IStakingManager(stakingManager).onSlashResult(_jobSlashed); + // when all chunks of Snapshots are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; _completeSubmission(_captureTimestamp); } - // - IStakingManager(stakingManager).onSlashResult(_jobSlashed); // TODO: unlock the selfStake and reward it to the transmitter } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index d5d61bc..d2b2b80 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -109,6 +109,7 @@ contract SymbioticStakingReward is } /// @notice called when inflation reward is generated + /// @dev this function is not called if there is no pending inflation reward in JobManager function updateInflationReward(address _operator, uint256 _rewardAmount) external onlySymbioticStaking { address[] memory stakeTokenLost = _getStakeTokenList(); for (uint256 i = 0; i < stakeTokenLost.length; i++) { @@ -122,7 +123,7 @@ contract SymbioticStakingReward is /// @notice update rewardPerToken and rewardAccrued for each vault /// @dev called when snapshot is submitted function onSnapshotSubmission(Struct.VaultSnapshot[] calldata _vaultSnapshots) external onlySymbioticStaking { - // TODO: this can be called redundantly as each snapshots can have same operator address + // TODO: this can be called redundantly as each snapshots can include same operator and vault address // update inlfationReward info of each vault address[] memory stakeTokenList = _getStakeTokenList(); From 99b3d3c15f35976c611a22467fba250f5d787ce4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 7 Oct 2024 19:56:53 +0900 Subject: [PATCH 130/158] format code --- .../staking/l2_contracts/NativeStaking.sol | 138 ++++++++++-------- .../staking/l2_contracts/StakingManager.sol | 38 ++--- .../staking/l2_contracts/SymbioticStaking.sol | 33 +++-- .../l2_contracts/SymbioticStakingReward.sol | 22 +-- 4 files changed, 118 insertions(+), 113 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 30443a5..7eeb3e4 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -17,7 +17,6 @@ import {INativeStakingReward} from "../../interfaces/staking/INativeStakingRewar import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - import {Struct} from "../../lib/staking/Struct.sol"; contract NativeStaking is @@ -62,6 +61,8 @@ contract NativeStaking is _; } + /*=================================================== initialize ====================================================*/ + function initialize(address _admin) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); @@ -71,6 +72,10 @@ contract NativeStaking is _grantRole(DEFAULT_ADMIN_ROLE, _admin); } + /*==================================================== external =====================================================*/ + + /*-------------------------------- Native Staking --------------------------------*/ + // Staker should be able to choose an Operator they want to stake into function stake(address _operator, address _stakeToken, uint256 _amount) external @@ -113,67 +118,8 @@ contract NativeStaking is // TODO } - /*======================================== Getters ========================================*/ - - function getStakeTokenList() external view returns (address[] memory) { - return stakeTokenSet.values(); - } - - function getStakeAmount(address _account, address _operator, address _stakeToken) external view returns (uint256) { - return stakeAmounts[_account][_operator][_stakeToken]; - } - - function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256) { - return operatorstakeAmounts[_operator][_token]; - } - - function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256) { - return _getOperatorActiveStakeAmount(_operator, _token); - } - - function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { - return operatorstakeAmounts[_operator][_token]; - } - - function _getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; - } - - function isSupportedToken(address _token) external view returns (bool) { - return stakeTokenSet.contains(_token); - } - - /*======================================== Admin ========================================*/ - - function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_isSupported) { - stakeTokenSet.add(_token); - } else { - stakeTokenSet.remove(_token); - } - - // TODO: emit event - } - - function setNativeStakingReward(address _nativeStakingReward) external onlyRole(DEFAULT_ADMIN_ROLE) { - rewardDistributor = _nativeStakingReward; + /*-------------------------------- Satking Manager -------------------------------*/ - // TODO: emit event - } - - function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { - stakingManager = _stakingManager; - - // TODO: emit event - } - - function setAmountToLock(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - amountToLock[_token] = _amount; - - // TODO: emit event - } - - /*======================================== StakingManager ========================================*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectTokenToLock(); uint256 _amountToLock = amountToLock[_token]; @@ -229,10 +175,31 @@ contract NativeStaking is _distributeInflationReward(_operator, _rewardAmount); } - function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { - return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); + /*================================================== external view ==================================================*/ + + function getStakeTokenList() external view returns (address[] memory) { + return stakeTokenSet.values(); + } + + function getStakeAmount(address _account, address _operator, address _stakeToken) external view returns (uint256) { + return stakeAmounts[_account][_operator][_stakeToken]; + } + + function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256) { + return operatorstakeAmounts[_operator][_token]; + } + + function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256) { + return _getOperatorActiveStakeAmount(_operator, _token); } + + function isSupportedToken(address _token) external view returns (bool) { + return stakeTokenSet.contains(_token); + } + + /*===================================================== internal ====================================================*/ + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); @@ -257,6 +224,12 @@ contract NativeStaking is delete lockInfo[_jobId]; } + /*============================================== internal view =============================================*/ + + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); + } + function _selectTokenToLock() internal view returns(address) { require(stakeTokenSet.length() > 0, "No supported token"); @@ -268,7 +241,44 @@ contract NativeStaking is return stakeTokenSet.at(idx); } - /*======================================== Overrides ========================================*/ + function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { + return operatorstakeAmounts[_operator][_token]; + } + + function _getOperatorActiveStakeAmount(address _operator, address _token) internal view returns (uint256) { + return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; + } + + /*====================================================== admin ======================================================*/ + + function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_isSupported) { + stakeTokenSet.add(_token); + } else { + stakeTokenSet.remove(_token); + } + + // TODO: emit event + } + + function setNativeStakingReward(address _nativeStakingReward) external onlyRole(DEFAULT_ADMIN_ROLE) { + rewardDistributor = _nativeStakingReward; + + // TODO: emit event + } + + function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { + stakingManager = _stakingManager; + + // TODO: emit event + } + + function setAmountToLock(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + amountToLock[_token] = _amount; + + // TODO: emit event + } + /*==================================================== overrides ====================================================*/ function supportsInterface(bytes4 interfaceId) public diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index d9cb6cc..7c3e2e4 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -84,6 +84,25 @@ contract StakingManager is // TODO: emit event } + /// @notice called by SymbioticStaking contract when slash result is submitted + function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { + // msg.sender will most likely be SymbioticStaking contract + require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); + + // refund fee to the requester + for(uint256 i = 0; i < _jobsSlashed.length; i++) { + // this can be done manually in the JobManager contract + // refunds nothing if already refunded + IJobManager(jobManager).refundFee(_jobsSlashed[i].jobId); + } + + uint256 len = stakingPoolSet.length(); + for (uint256 i = 0; i < len; i++) { + address pool = stakingPoolSet.at(i); + IStakingPool(pool).slash(_jobsSlashed); + } + } + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyJobManager { if(_rewardAmount == 0) return; @@ -108,25 +127,6 @@ contract StakingManager is return (poolFeeRewardAmount, poolInflationRewardAmount); } - /// @notice called by SymbioticStaking contract when slash result is submitted - function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { - // msg.sender will most likely be SymbioticStaking contract - require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); - - // refund fee to the requester - for(uint256 i = 0; i < _jobsSlashed.length; i++) { - // this can be done manually in the JobManager contract - // refunds nothing if already refunded - IJobManager(jobManager).refundFee(_jobsSlashed[i].jobId); - } - - uint256 len = stakingPoolSet.length(); - for (uint256 i = 0; i < len; i++) { - address pool = stakingPoolSet.at(i); - IStakingPool(pool).slash(_jobsSlashed); - } - } - /*======================================== Getters ========================================*/ function isEnabledPool(address _pool) public view returns (bool) { return poolConfig[_pool].enabled; diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index aed2808..6dd086a 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -54,7 +54,6 @@ contract SymbioticStaking is /* Symbiotic Snapshot */ - // to track if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; // to track if all partial txs are received @@ -91,9 +90,9 @@ contract SymbioticStaking is rewardDistributor = _rewardDistributor; } - /*============================================= external functions =============================================*/ + /*===================================================== external ====================================================*/ - /*------------------------- L1 to L2 submission ------------------------- */ + /*------------------------------ L1 to L2 submission -----------------------------*/ function submitVaultSnapshot( uint256 _index, @@ -160,7 +159,7 @@ contract SymbioticStaking is // TODO: unlock the selfStake and reward it to the transmitter } - /*------------------------- stake lock/unlock for job -------------------------*/ + /*--------------------------- stake lock/unlock for job --------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); @@ -204,7 +203,7 @@ contract SymbioticStaking is // TODO: emit event } - /*------------------------- slash -------------------------*/ + /*------------------------------------- slash ------------------------------------*/ function slash(Struct.JobSlashed[] calldata _slashedJobs) external onlyStakingManager { uint256 len = _slashedJobs.length; @@ -230,9 +229,9 @@ contract SymbioticStaking is } } - /*======================================== internal functions ========================================*/ + /*===================================================== internal ====================================================*/ - /*------------------------- Snapshot Submission -------------------------*/ + /*------------------------------- Snapshot Submission ----------------------------*/ function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { if(registeredTransmitters[_captureTimestamp] == address(0)) { @@ -280,7 +279,7 @@ contract SymbioticStaking is // TODO: emit event } - /*------------------------- Reward Distribution -------------------------*/ + /*------------------------------ Reward Distribution -----------------------------*/ function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); @@ -293,8 +292,9 @@ contract SymbioticStaking is ISymbioticStakingReward(rewardDistributor).updateInflationReward(_operator, _amount); } - /*======================================== external view functions ========================================*/ - + /*================================================== external view ==================================================*/ + + function lastConfirmedTimestamp() public view returns (uint256) { return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; } @@ -321,9 +321,9 @@ contract SymbioticStaking is return stakeTokenSet.contains(_token); } - /*======================================== internal view functions ========================================*/ + /*================================================== internal view ==================================================*/ - /*--------------- Snapshot Submission ---------------*/ + /*------------------------------ Snapshot Submission -----------------------------*/ function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { require(_numOfTxs > 0, "Invalid length"); @@ -357,7 +357,7 @@ contract SymbioticStaking is // TODO: (block.timestamp - _lastConfirmedTimestamp) * X } - /*------------------------- Job -------------------------*/ + /*-------------------------------------- Job -------------------------------------*/ function _selectLockToken() internal view returns(address) { require(stakeTokenSet.length() > 0, "No supported token"); @@ -374,13 +374,14 @@ contract SymbioticStaking is // TODO: implement logic } - /*------------------------- Reward -------------------------*/ + /*------------------------------------ Reward ------------------------------------*/ function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } - /*======================================== Admin ========================================*/ + /*====================================================== admin ======================================================*/ + function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingManager = _stakingManager; @@ -407,7 +408,7 @@ contract SymbioticStaking is // TODO: emit event } - /*======================================== Overrides ========================================*/ + /*==================================================== overrides ====================================================*/ function supportsInterface(bytes4 interfaceId) public diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index d2b2b80..791d7b1 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -18,11 +18,6 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Struct} from "../../lib/staking/Struct.sol"; -/* - Unlike common staking contracts, this contract is interacted each time snapshot is submitted to Symbiotic Staking, - which means the state of each vault address will be updated whenvever snapshot is submitted. -*/ - contract SymbioticStakingReward is ContextUpgradeable, ERC165Upgradeable, @@ -70,7 +65,8 @@ contract SymbioticStakingReward is _; } - /*============================================= init =============================================*/ + /*=============================================== initialize ===============================================*/ + function initialize( address _admin, address _jobManager, @@ -94,7 +90,7 @@ contract SymbioticStakingReward is inflationRewardToken = _inflationRewardToken; } - /*============================================= external functions =============================================*/ + /*================================================ external ================================================*/ /* ------------------------- reward update ------------------------- */ @@ -158,14 +154,14 @@ contract SymbioticStakingReward is rewardAccrued[_msgSender()][inflationRewardToken] = 0; } - /*============================================= external view functions =============================================*/ + /*================================================== external view ==================================================*/ function getVaultRewardAccrued(address _vault) external view returns (uint256 feeReward, uint256 inflationReward) { // TODO: this does not include pending inflation reward as it requires states update in JobManager return (rewardAccrued[_vault][feeRewardToken], rewardAccrued[_vault][inflationRewardToken]); } - /*============================================= internal functions =============================================*/ + /*===================================================== internal ====================================================*/ /// @dev update pending inflation reward and update rewardPerTokenStored of the operator function _updatePendingInflationReward(address _operator) internal { @@ -191,7 +187,7 @@ contract SymbioticStakingReward is } } - /*============================================= internal view functions =============================================*/ + /*================================================== internal view ==================================================*/ function _getStakeTokenList() internal view returns (address[] memory) { return ISymbioticStaking(symbioticStaking).getStakeTokenList(); @@ -205,9 +201,7 @@ contract SymbioticStakingReward is return ISymbioticStaking(symbioticStaking).getStakeAmount(_vault, _stakeToken, _operator); } - /*============================================= internal pure functions =============================================*/ - - /*======================================== admin functions ========================================*/ + /*======================================================= admin =====================================================*/ function setStakingPool(address _symbioticStaking) public onlyRole(DEFAULT_ADMIN_ROLE) { symbioticStaking = _symbioticStaking; @@ -225,7 +219,7 @@ contract SymbioticStakingReward is inflationRewardToken = _inflationRewardToken; } - /*======================================== overrides ========================================*/ + /*===================================================== overrides ===================================================*/ function supportsInterface(bytes4 interfaceId) public From cc59e387aee46a0b78c0ba5c21846914cf352ac3 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 7 Oct 2024 20:42:13 +0900 Subject: [PATCH 131/158] add 2step withdrawal --- contracts/lib/staking/Struct.sol | 39 ++++---- .../staking/l2_contracts/NativeStaking.sol | 91 ++++++++++++++----- 2 files changed, 88 insertions(+), 42 deletions(-) diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 7d32d49..28330d8 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; library Struct { - /* Job Manager */ + /*=========================== Job Manager =============================*/ struct JobInfo { address requester; address operator; @@ -11,14 +11,23 @@ library Struct { uint256 deadline; } - /* Staking Pool */ + /*========================= Staking Manager ===========================*/ + + struct PoolConfig { + uint256 weight; + bool enabled; + } + + /*=========================== Staking Pool ============================*/ + struct PoolLockInfo { address token; uint256 amount; address transmitter; } - /* NativeStaking */ + /*========================== Native Staking ===========================*/ + struct NativeStakingLock { address token; uint256 amount; @@ -30,14 +39,14 @@ library Struct { address rewardAddress; } - struct SnapshotTxCountInfo { - uint256 idxToSubmit; // idx of pratial snapshot tx to submit - uint256 numOfTxs; // total number of txs for the snapshot + struct WithdrawalRequest { + address stakeToken; + uint256 amount; + uint256 withdrawalTime; } - /*==================== Symbiotic Staking ==================== */ + /*========================= Symbiotic Staking =========================*/ - /* Snapshot Submission */ struct VaultSnapshot { address operator; address vault; @@ -45,13 +54,17 @@ library Struct { uint256 stakeAmount; } + struct SnapshotTxCountInfo { + uint256 idxToSubmit; // idx of pratial snapshot tx to submit + uint256 numOfTxs; // total number of txs for the snapshot + } + struct ConfirmedTimestamp { uint256 captureTimestamp; address transmitter; uint256 transmitterComissionRate; } - /* Job Lock */ struct SymbioticStakingLock { address stakeToken; uint256 amount; @@ -60,12 +73,4 @@ library Struct { uint256 transmitterComissionRate; } - struct PoolConfig { - uint256 weight; - bool enabled; - } - struct RewardPerToken { - uint256 value; - uint256 lastUpdatedTimestamp; //? not sure if this actually saves gas - } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 7eeb3e4..36d8efe 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -38,14 +38,19 @@ contract NativeStaking is address public inflationRewardToken; /* Config */ + uint256 public withdrawalDuration; mapping(address stakeToken => uint256 lockAmount) public amountToLock; // amount of token to lock for each job creation mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% /* Stake */ // total staked amounts for each operator - mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorstakeAmounts; + mapping(address operator => mapping(address stakeToken => uint256 stakeAmounts)) public operatorstakeAmounts; // staked amount for each account - mapping(address account => mapping(address operator => mapping(address token => uint256 amount))) public stakeAmounts; + mapping(address account => mapping(address operator => mapping(address stakeToken => uint256 amount))) public + stakeAmounts; + + mapping(address account => mapping(address operator => Struct.WithdrawalRequest[] withdrawalRequest)) public + withdrawalRequests; /* Locked Stakes */ mapping(uint256 jobId => Struct.NativeStakingLock lock) public lockInfo; @@ -63,13 +68,26 @@ contract NativeStaking is /*=================================================== initialize ====================================================*/ - function initialize(address _admin) public initializer { + function initialize( + address _admin, + address _stakingManager, + address _rewardDistributor, + uint256 _withdrawalDuration, + address _feeToken, + address _inflationRewardToken + ) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); __UUPSUpgradeable_init_unchained(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + stakingManager = _stakingManager; + rewardDistributor = _rewardDistributor; + withdrawalDuration = _withdrawalDuration; + feeRewardToken = _feeToken; + inflationRewardToken = _inflationRewardToken; } /*==================================================== external =====================================================*/ @@ -90,7 +108,7 @@ contract NativeStaking is stakeAmounts[msg.sender][_operator][_stakeToken] += _amount; operatorstakeAmounts[_operator][_stakeToken] += _amount; - INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); + // INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); emit Staked(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } @@ -103,18 +121,19 @@ contract NativeStaking is stakeAmounts[msg.sender][_operator][_stakeToken] -= _amount; operatorstakeAmounts[_operator][_stakeToken] -= _amount; - IERC20(_stakeToken).safeTransfer(msg.sender, _amount); + withdrawalRequests[msg.sender][_operator].push( + Struct.WithdrawalRequest(_stakeToken, _amount, block.timestamp + withdrawalDuration) + ); - INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); + // INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); emit StakeWithdrawn(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } - function withdrawStake(address _operator, address _stakeToken) external nonReentrant { + function withdrawStake(address _operator, uint256[] calldata _index) external nonReentrant { require(msg.sender == _operator, "Only operator can withdraw stake"); - uint256 _amount = stakeAmounts[msg.sender][_operator][_stakeToken]; - require(_amount > 0, "No stake to withdraw"); + _withdrawStake(_operator, _index); // TODO } @@ -132,21 +151,26 @@ contract NativeStaking is // TODO: emit event } - /// @notice unlock stake and distribute reward + /// @notice unlock stake and distribute reward /// @dev called by StakingManager when job is completed - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { + function onJobCompletion( + uint256 _jobId, + address _operator, + uint256 _feeRewardAmount, + uint256 _inflationRewardAmount + ) external onlyStakingManager { Struct.NativeStakingLock memory lock = lockInfo[_jobId]; - if(lock.amount == 0) return; + if (lock.amount == 0) return; _unlockStake(_jobId, _operator, lock.token, lock.amount); // distribute fee reward - if(_feeRewardAmount > 0){ + if (_feeRewardAmount > 0) { _distributeFeeReward(lock.token, _operator, _feeRewardAmount); } - if(_inflationRewardAmount > 0) { + if (_inflationRewardAmount > 0) { _distributeInflationReward(_operator, _inflationRewardAmount); } @@ -159,18 +183,18 @@ contract NativeStaking is Struct.NativeStakingLock memory lock = lockInfo[_slashedJobs[i].jobId]; uint256 lockedAmount = lock.amount; - if(lockedAmount == 0) continue; // if already slashed + if (lockedAmount == 0) continue; // if already slashed _unlockStake(_slashedJobs[i].jobId, _slashedJobs[i].operator, lock.token, lockedAmount); IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); - INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, lock.token, _slashedJobs[i].operator); + // INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, lock.token, _slashedJobs[i].operator); } // TODO: emit event } function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { - if(_rewardAmount == 0) return; + if (_rewardAmount == 0) return; _distributeInflationReward(_operator, _rewardAmount); } @@ -193,16 +217,15 @@ contract NativeStaking is return _getOperatorActiveStakeAmount(_operator, _token); } - function isSupportedToken(address _token) external view returns (bool) { return stakeTokenSet.contains(_token); } /*===================================================== internal ====================================================*/ - + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); + IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); } function _distributeInflationReward(address _operator, uint256 _rewardAmount) internal { @@ -210,7 +233,7 @@ contract NativeStaking is address[] memory stakeTokens = stakeTokenSet.values(); uint256[] memory rewardAmounts = new uint256[](len); uint256 inflationRewardAmount; - for(uint256 i = 0; i < len; i++) { + for (uint256 i = 0; i < len; i++) { rewardAmounts[i] = _calcInflationRewardAmount(stakeTokens[i], _rewardAmount); inflationRewardAmount += rewardAmounts[i]; } @@ -224,15 +247,33 @@ contract NativeStaking is delete lockInfo[_jobId]; } + function _withdrawStake(address _operator, uint256[] calldata _index) internal { + for (uint256 i = 0; i < _index.length; i++) { + Struct.WithdrawalRequest memory request = withdrawalRequests[msg.sender][_operator][_index[i]]; + + require(request.withdrawalTime <= block.timestamp, "Withdrawal time not reached"); + + require(request.amount > 0, "Invalid withdrawal request"); + + withdrawalRequests[msg.sender][_operator][_index[i]].amount = 0; + + IERC20(request.stakeToken).safeTransfer(msg.sender, request.amount); + } + } + /*============================================== internal view =============================================*/ - function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) + internal + view + returns (uint256) + { return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } - function _selectTokenToLock() internal view returns(address) { + function _selectTokenToLock() internal view returns (address) { require(stakeTokenSet.length() > 0, "No supported token"); - + uint256 idx; if (stakeTokenSet.length() > 1) { uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); @@ -244,7 +285,7 @@ contract NativeStaking is function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { return operatorstakeAmounts[_operator][_token]; } - + function _getOperatorActiveStakeAmount(address _operator, address _token) internal view returns (uint256) { return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; } From e74888c67df16c389683c03eeade9129a11239e2 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 7 Oct 2024 22:05:56 +0900 Subject: [PATCH 132/158] fix typo --- contracts/staking/l2_contracts/RewardDistributor.sol | 8 ++++---- contracts/staking/l2_contracts/SymbioticStakingReward.sol | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/staking/l2_contracts/RewardDistributor.sol b/contracts/staking/l2_contracts/RewardDistributor.sol index 084b16e..1e3c794 100644 --- a/contracts/staking/l2_contracts/RewardDistributor.sol +++ b/contracts/staking/l2_contracts/RewardDistributor.sol @@ -105,10 +105,10 @@ abstract contract RewardDistributor is /// @notice called when inflation reward is generated function updateInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingPool { - address[] memory stakeTokenLost = _getStakeTokenList(); - for(uint256 i = 0; i < stakeTokenLost.length; i++) { - rewards[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount; - rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); + address[] memory stakeTokenList = _getStakeTokenList(); + for(uint256 i = 0; i < stakeTokenList.length; i++) { + rewards[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount; + rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); } } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 791d7b1..4cc82d5 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -107,10 +107,10 @@ contract SymbioticStakingReward is /// @notice called when inflation reward is generated /// @dev this function is not called if there is no pending inflation reward in JobManager function updateInflationReward(address _operator, uint256 _rewardAmount) external onlySymbioticStaking { - address[] memory stakeTokenLost = _getStakeTokenList(); - for (uint256 i = 0; i < stakeTokenLost.length; i++) { - rewardPerTokenStored[stakeTokenLost[i]][_operator][inflationRewardToken] += - _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenLost[i])); + address[] memory stakeTokenList = _getStakeTokenList(); + for (uint256 i = 0; i < stakeTokenList.length; i++) { + rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += + _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); } } From 38fed2da51241583dcf1cd29e0f997f7406f4344 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 7 Oct 2024 22:08:48 +0900 Subject: [PATCH 133/158] resolve compile error --- .../interfaces/staking/IRewardDistributor.sol | 4 +- .../staking/l2_contracts/NativeStaking.sol | 50 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/contracts/interfaces/staking/IRewardDistributor.sol b/contracts/interfaces/staking/IRewardDistributor.sol index ce251d6..7d797c6 100644 --- a/contracts/interfaces/staking/IRewardDistributor.sol +++ b/contracts/interfaces/staking/IRewardDistributor.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.26; interface IRewardDistributor { - function addFeeReward(address _stakeToken, address _operator, uint256 _amount) external; + function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external; - function addInflationReward(address operator, address[] calldata stakeTokens, uint256[] calldata rewardAmounts) external; + function updateInflationReward(address _operator, uint256 _rewardAmount) external; function onStakeUpdate(address _account, address _stakeToken, address _operator) external; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 36d8efe..58fd170 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -166,13 +166,13 @@ contract NativeStaking is _unlockStake(_jobId, _operator, lock.token, lock.amount); // distribute fee reward - if (_feeRewardAmount > 0) { - _distributeFeeReward(lock.token, _operator, _feeRewardAmount); - } + // if (_feeRewardAmount > 0) { + // _distributeFeeReward(lock.token, _operator, _feeRewardAmount); + // } - if (_inflationRewardAmount > 0) { - _distributeInflationReward(_operator, _inflationRewardAmount); - } + // if (_inflationRewardAmount > 0) { + // _distributeInflationReward(_operator, _inflationRewardAmount); + // } // TODO: emit event } @@ -196,7 +196,7 @@ contract NativeStaking is function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { if (_rewardAmount == 0) return; - _distributeInflationReward(_operator, _rewardAmount); + // _distributeInflationReward(_operator, _rewardAmount); } /*================================================== external view ==================================================*/ @@ -223,24 +223,24 @@ contract NativeStaking is /*===================================================== internal ====================================================*/ - function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); - } - - function _distributeInflationReward(address _operator, uint256 _rewardAmount) internal { - uint256 len = stakeTokenSet.length(); - address[] memory stakeTokens = stakeTokenSet.values(); - uint256[] memory rewardAmounts = new uint256[](len); - uint256 inflationRewardAmount; - for (uint256 i = 0; i < len; i++) { - rewardAmounts[i] = _calcInflationRewardAmount(stakeTokens[i], _rewardAmount); - inflationRewardAmount += rewardAmounts[i]; - } - - IERC20(inflationRewardToken).safeTransfer(rewardDistributor, inflationRewardAmount); - IRewardDistributor(rewardDistributor).addInflationReward(_operator, stakeTokens, rewardAmounts); - } + // function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { + // IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); + // IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); + // } + + // function _distributeInflationReward(address _operator, uint256 _rewardAmount) internal { + // uint256 len = stakeTokenSet.length(); + // address[] memory stakeTokens = stakeTokenSet.values(); + // uint256[] memory rewardAmounts = new uint256[](len); + // uint256 inflationRewardAmount; + // for (uint256 i = 0; i < len; i++) { + // rewardAmounts[i] = _calcInflationRewardAmount(stakeTokens[i], _rewardAmount); + // inflationRewardAmount += rewardAmounts[i]; + // } + + // IERC20(inflationRewardToken).safeTransfer(rewardDistributor, inflationRewardAmount); + // IRewardDistributor(rewardDistributor).addInflationReward(_operator, stakeTokens, rewardAmounts); + // } function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { operatorLockedAmounts[_operator][_stakeToken] -= _amount; From c6caeb0bee90bcd0a0b5123f9ee172c5e7b5268e Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 7 Oct 2024 22:14:53 +0900 Subject: [PATCH 134/158] remove totalFeeStored --- contracts/staking/l2_contracts/JobManager.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 36722d8..6e41e70 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -37,7 +37,6 @@ contract JobManager is address public inflationRewardToken; uint256 public jobDuration; - uint256 public totalFeeStored; // TODO: check if needed uint256 inflationRewardEpochSize; uint256 inflationRewardPerEpoch; @@ -89,8 +88,6 @@ contract JobManager is IStakingManager(stakingManager).onJobCreation(_jobId, _operator); - totalFeeStored += _feeAmount; - // TODO: emit event } @@ -103,7 +100,7 @@ contract JobManager is _verifyProof(_jobId, _proof); uint256 feePaid = jobs[_jobId].feePaid; - uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); + uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); // TODO: move to StakingManager // send fee and unlock stake // TODO: consider where the fund comes from @@ -148,7 +145,6 @@ contract JobManager is require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); jobs[_jobId].feePaid = 0; - totalFeeStored -= jobs[_jobId].feePaid; IERC20(feeToken).safeTransfer(jobs[_jobId].requester, jobs[_jobId].feePaid); From 0d11aa981710f11264e9f5432f482b971653557b Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 11:13:29 +0900 Subject: [PATCH 135/158] symbioticStakingReward pulls reward --- contracts/staking/l2_contracts/JobManager.sol | 5 +---- contracts/staking/l2_contracts/NativeStaking.sol | 2 +- contracts/staking/l2_contracts/StakingManager.sol | 4 +--- contracts/staking/l2_contracts/SymbioticStaking.sol | 3 +-- contracts/staking/l2_contracts/SymbioticStakingReward.sol | 8 +++++--- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 6e41e70..d9f27e4 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -103,12 +103,9 @@ contract JobManager is uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); // TODO: move to StakingManager // send fee and unlock stake - // TODO: consider where the fund comes from - IERC20(feeToken).safeTransfer(stakingManager, feePaid); // TODO: make RewardDistributor pull fee from JobManager - IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); - _updateJobCompletionEpoch(_jobId); // TODO + _updateJobCompletionEpoch(_jobId); } /** diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 58fd170..b8b3995 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -210,7 +210,7 @@ contract NativeStaking is } function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256) { - return operatorstakeAmounts[_operator][_token]; + return _getOperatorStakeAmount(_operator, _token); } function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256) { diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 7c3e2e4..0c25919 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -77,9 +77,7 @@ contract StakingManager is (uint256 poolFeeRewardAmount, uint256 poolInflationRewardAmount) = _calcRewardAmount(pool, _feePaid, _inflationRewardAmount); - IERC20(feeToken).safeTransfer(pool, poolFeeRewardAmount); - IERC20(inflationRewardToken).safeTransfer(pool, poolInflationRewardAmount); - IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, _inflationRewardAmount); + IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, poolInflationRewardAmount); } // TODO: emit event } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 6dd086a..dfba556 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -282,13 +282,11 @@ contract SymbioticStaking is /*------------------------------ Reward Distribution -----------------------------*/ function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); ISymbioticStakingReward(rewardDistributor).updateFeeReward(_stakeToken, _operator, _amount); } function _distributeInflationReward(address _operator, uint256 _amount) internal { - IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); ISymbioticStakingReward(rewardDistributor).updateInflationReward(_operator, _amount); } @@ -359,6 +357,7 @@ contract SymbioticStaking is /*-------------------------------------- Job -------------------------------------*/ + // TODO: weight based random selection function _selectLockToken() internal view returns(address) { require(stakeTokenSet.length() > 0, "No supported token"); diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 4cc82d5..9963b75 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -99,9 +99,10 @@ contract SymbioticStakingReward is function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlySymbioticStaking - { + { rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); + IERC20(feeRewardToken).safeTransferFrom(jobManager, address(this), _rewardAmount); } /// @notice called when inflation reward is generated @@ -112,6 +113,7 @@ contract SymbioticStakingReward is rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); } + IERC20(inflationRewardToken).safeTransferFrom(jobManager, address(this), _rewardAmount); } /* ------------------------- symbiotic staking ------------------------- */ @@ -146,11 +148,11 @@ contract SymbioticStakingReward is // TODO: check transfer logic // transfer fee reward to the vault - IERC20(feeRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][feeRewardToken]); + IERC20(feeRewardToken).safeTransferFrom(jobManager, _msgSender(), rewardAccrued[_msgSender()][feeRewardToken]); rewardAccrued[_msgSender()][feeRewardToken] = 0; // transfer inflation reward to the vault - IERC20(inflationRewardToken).safeTransfer(_msgSender(), rewardAccrued[_msgSender()][inflationRewardToken]); + IERC20(inflationRewardToken).safeTransferFrom(jobManager, _msgSender(), rewardAccrued[_msgSender()][inflationRewardToken]); rewardAccrued[_msgSender()][inflationRewardToken] = 0; } From 5f8a4d339a0036f37d3c2c658a672e36c3c9229a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 18:14:23 +0900 Subject: [PATCH 136/158] initialize foundry --- .gitmodules | 3 +++ .vscode/settings.json | 3 --- foundry.toml | 13 +++++++++++++ lib/forge-std | 1 + remappings.txt | 4 ++++ 5 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .gitmodules delete mode 100644 .vscode/settings.json create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 100644 remappings.txt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e46926d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" -} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..083817d --- /dev/null +++ b/foundry.toml @@ -0,0 +1,13 @@ +[profile.default] +src = "contracts" +out = "artifacts" +libs = ["lib", "node_modules"] +remappings = [ + "@openzeppelin/=node_modules/@openzeppelin/", + "eth-gas-reporter/=node_modules/eth-gas-reporter/", + "hardhat/=node_modules/hardhat/", + "forge-std/=lib/forge-std/src/", +] +auto_detect_solc = true + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8f24d6b --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..864ddd1 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,4 @@ +@openzeppelin/=node_modules/@openzeppelin/ +eth-gas-reporter/=node_modules/eth-gas-reporter/ +hardhat/=node_modules/hardhat/ +forge-std/=lib/forge-std/src/ From 77ca2dab1a73effc228402e122d152690c72712f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 23:17:14 +0900 Subject: [PATCH 137/158] add comment --- contracts/staking/l2_contracts/JobManager.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index d9f27e4..c278063 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -46,6 +46,9 @@ contract JobManager is // idx of operatorJobCompletionEpochs, inflationReward distribution should be reflected from this idx mapping(address operator => uint256 idx) inflationRewardEpochBeginIdx; + // TODO: temporary + mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% + // count of jobs done by operator in an epoch mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCount; // total count of jobs done in an epoch @@ -75,7 +78,8 @@ contract JobManager is function createJob(uint256 _jobId, address _requester, address _operator, uint256 _feeAmount) external nonReentrant - { + { + // TODO: this should be removed IERC20(feeToken).safeTransferFrom(_requester, address(this), _feeAmount); // stakeToken and lockAmount will be decided in each pool From 696b8e741796c177f9b2bd1879f05b3f225259e4 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 23:17:39 +0900 Subject: [PATCH 138/158] remove internal change to add public --- contracts/interfaces/staking/IStakingPool.sol | 2 +- .../staking/l2_contracts/NativeStaking.sol | 36 ++++++++++--------- .../staking/l2_contracts/SymbioticStaking.sol | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index fc1af14..23a6dcc 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import {Struct} from "../../lib/staking/Struct.sol"; interface IStakingPool { - function isSupportedToken(address _token) external view returns (bool); + function isSupportedStakeToken(address _token) external view returns (bool); function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index b8b3995..2654e60 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -116,7 +116,7 @@ contract NativeStaking is // This should update StakingManger's state // TODO function requestStakeWithdrawal(address _operator, address _stakeToken, uint256 _amount) external nonReentrant { - require(_getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); + require(getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); stakeAmounts[msg.sender][_operator][_stakeToken] -= _amount; operatorstakeAmounts[_operator][_stakeToken] -= _amount; @@ -142,7 +142,7 @@ contract NativeStaking is function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectTokenToLock(); uint256 _amountToLock = amountToLock[_token]; - require(_getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); + require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); // lock stake lockInfo[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); @@ -199,6 +199,16 @@ contract NativeStaking is // _distributeInflationReward(_operator, _rewardAmount); } + /*==================================================== public view ==================================================*/ + + function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorstakeAmounts[_operator][_token]; + } + + function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { + return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; + } + /*================================================== external view ==================================================*/ function getStakeTokenList() external view returns (address[] memory) { @@ -209,15 +219,7 @@ contract NativeStaking is return stakeAmounts[_account][_operator][_stakeToken]; } - function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256) { - return _getOperatorStakeAmount(_operator, _token); - } - - function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256) { - return _getOperatorActiveStakeAmount(_operator, _token); - } - - function isSupportedToken(address _token) external view returns (bool) { + function isSupportedStakeToken(address _token) public view returns (bool) { return stakeTokenSet.contains(_token); } @@ -282,13 +284,13 @@ contract NativeStaking is return stakeTokenSet.at(idx); } - function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { - return operatorstakeAmounts[_operator][_token]; - } + // function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { + // return operatorstakeAmounts[_operator][_token]; + // } - function _getOperatorActiveStakeAmount(address _operator, address _token) internal view returns (uint256) { - return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; - } + // function _getOperatorActiveStakeAmount(address _operator, address _token) internal view returns (uint256) { + // return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; + // } /*====================================================== admin ======================================================*/ diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index dfba556..f293f3e 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -315,7 +315,7 @@ contract SymbioticStaking is return stakeTokenSet.values(); } - function isSupportedToken(address _token) public view returns (bool) { + function isSupportedStakeToken(address _token) public view returns (bool) { return stakeTokenSet.contains(_token); } From fb9aed3a1bea24140d493e25809168042595843f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 23:18:34 +0900 Subject: [PATCH 139/158] refactor code --- contracts/staking/l2_contracts/SymbioticStaking.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index f293f3e..14b5fa9 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -302,7 +302,7 @@ contract SymbioticStaking is } function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - uint256 operatorStakeAmount = operatorStakeAmounts[lastConfirmedTimestamp()][_operator][_token]; + uint256 operatorStakeAmount = getOperatorStakeAmount(_operator, _token); uint256 operatorLockedAmount = operatorLockedAmounts[_operator][_token]; return operatorStakeAmount > operatorLockedAmount ? operatorStakeAmount - operatorLockedAmount : 0; } From 974df18e6c75db5602bcc5ad2eb9432c4b6fdcfb Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Tue, 8 Oct 2024 23:22:20 +0900 Subject: [PATCH 140/158] refactor SymbioticStakingLock --- contracts/lib/staking/Struct.sol | 5 +---- contracts/staking/l2_contracts/SymbioticStaking.sol | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 28330d8..700717a 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -68,9 +68,6 @@ library Struct { struct SymbioticStakingLock { address stakeToken; uint256 amount; - // transmitter who submitted with confirmedTimestamp used when job is created - address transmitter; - uint256 transmitterComissionRate; + uint256 timestampIdx; } - } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 14b5fa9..b525157 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -169,7 +169,7 @@ contract SymbioticStaking is uint256 timestampIdx = confirmedTimestamps.length - 1; address transmitter = confirmedTimestamps[timestampIdx].transmitter; - lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], transmitter, confirmedTimestamps[timestampIdx].transmitterComissionRate); + lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], timestampIdx); operatorLockedAmounts[_operator][_token] += amountToLock[_token]; // TODO: emit event @@ -178,8 +178,8 @@ contract SymbioticStaking is function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; - uint256 transmitterComissionRate = lock.transmitterComissionRate; - uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, transmitterComissionRate, 1e18); + uint256 timestampIdx = lock.timestampIdx; + uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[timestampIdx].transmitterComissionRate, 1e18); uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; @@ -194,7 +194,7 @@ contract SymbioticStaking is } // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation - IERC20(feeRewardToken).safeTransfer(lock.transmitter, transmitterComission); + IERC20(feeRewardToken).safeTransfer(confirmedTimestamps[timestampIdx].transmitter, transmitterComission); // unlock the stake locked during job creation delete lockInfo[_jobId]; From 4b74dbd403515ffb1a3642780ee07d2a1a13bf00 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 9 Oct 2024 00:47:17 +0900 Subject: [PATCH 141/158] remove epoch array and replace with single variable --- contracts/interfaces/staking/IJobManager.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 114 +++++------------- 2 files changed, 28 insertions(+), 88 deletions(-) diff --git a/contracts/interfaces/staking/IJobManager.sol b/contracts/interfaces/staking/IJobManager.sol index 40f9d81..26e9237 100644 --- a/contracts/interfaces/staking/IJobManager.sol +++ b/contracts/interfaces/staking/IJobManager.sol @@ -8,5 +8,5 @@ interface IJobManager { function refundFee(uint256 jobId) external; - function updateInflationReward(address _operator) external; + // function updateInflationReward(address _operator) external; } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index c278063..22bec75 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -32,6 +32,8 @@ contract JobManager is mapping(uint256 jobId => Struct.JobInfo jobInfo) public jobs; + uint256 public startTime; // TODO: immutable? + address public stakingManager; address public feeToken; address public inflationRewardToken; @@ -41,10 +43,12 @@ contract JobManager is uint256 inflationRewardEpochSize; uint256 inflationRewardPerEpoch; - // epochs in which operator has done jobs - mapping(address operator => uint256[] epochs) operatorJobCompletionEpochs; - // idx of operatorJobCompletionEpochs, inflationReward distribution should be reflected from this idx - mapping(address operator => uint256 idx) inflationRewardEpochBeginIdx; + // // epochs in which operator has done jobs + // mapping(address operator => uint256[] epochs) operatorJobCompletionEpochs; + // // idx of operatorJobCompletionEpochs, inflationReward distribution should be reflected from this idx + // mapping(address operator => uint256 idx) inflationRewardEpochBeginIdx; + + mapping(address operator => uint256 lastJobCompletionEpoch) opeartorLastJobCompletionEpochs; // TODO: temporary mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% @@ -56,7 +60,7 @@ contract JobManager is /*======================================== Init ========================================*/ - function initialize(address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) + function initialize(uint256 _startTime, address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) public initializer { @@ -67,6 +71,7 @@ contract JobManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); + startTime = _startTime; stakingManager = _stakingManager; feeToken = _feeToken; jobDuration = _jobDuration; @@ -104,7 +109,7 @@ contract JobManager is _verifyProof(_jobId, _proof); uint256 feePaid = jobs[_jobId].feePaid; - uint256 pendingInflationReward = _updateInflationReward(jobs[_jobId].operator); // TODO: move to StakingManager + uint256 pendingInflationReward = updateInflationReward(jobs[_jobId].operator); // TODO: move to StakingManager // send fee and unlock stake IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); @@ -128,11 +133,11 @@ contract JobManager is } function _updateJobCompletionEpoch(uint256 _jobId) internal { - uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; - uint256 len = operatorJobCompletionEpochs[jobs[_jobId].operator].length; - - if(len > 0 && operatorJobCompletionEpochs[jobs[_jobId].operator][len - 1] != currentEpoch) { - operatorJobCompletionEpochs[jobs[_jobId].operator].push(currentEpoch); + uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; + address operator = jobs[_jobId].operator; + + if(opeartorLastJobCompletionEpochs[operator] != currentEpoch) { + opeartorLastJobCompletionEpochs[operator] = currentEpoch; } } @@ -158,98 +163,33 @@ contract JobManager is /// @notice update inflation reward for operator /// @dev can be called by anyone, but most likely when proof is submitted(when job is completed) by operator /// @dev or inflation reward is claimed in a RewardDistributor - function updateInflationReward(address _operator) external { - uint256 pendingInflationReward = _updateInflationReward(_operator); + function updateInflationReward(address _operator) public returns(uint256 pendingInflationReward) { + pendingInflationReward = getPendingInflationReward(_operator); if(pendingInflationReward > 0) { - // send reward to StakingManager - IERC20(inflationRewardToken).safeTransfer(stakingManager, pendingInflationReward); // and distribute IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); } } - function getPendingInflationReward(address _operator) external view returns(uint256) { - return _getPendingInflationReward(_operator); - } - - /*======================================== Internal functions ========================================*/ - - function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { - // TODO: verify proof - - // TODO: emit event - } - /// @notice update pending inflation reward for operator - function _updateInflationReward(address _operator) internal returns(uint256 pendingInflationReward) { - // check if operator has completed any job - if (operatorJobCompletionEpochs[_operator].length == 0) return 0; - - // list of epochs in which operator has completed jobs - uint256[] storage completedEpochs = operatorJobCompletionEpochs[_operator]; - - // first epoch which the reward has not been distributed - uint256 beginIdx = inflationRewardEpochBeginIdx[_operator]; - - // no job completed since last update - if(beginIdx > completedEpochs.length) return 0; + function getPendingInflationReward(address _operator) public view returns(uint256 pendingInflationReward) { + uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; + uint256 lastEpoch = opeartorLastJobCompletionEpochs[_operator]; - uint256 beginEpoch = completedEpochs[beginIdx]; - uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; + if(lastEpoch == currentEpoch) return 0; - // no pending reward if operator has already claimed reward until latest epoch - if(beginEpoch == currentEpoch) return 0; - - // update pending reward - uint256 rewardPerEpoch = inflationRewardPerEpoch; // cache - uint256 len = completedEpochs.length; - - for(uint256 idx = beginIdx; idx < len; idx++) { - uint256 epoch = completedEpochs[idx]; - - // for last epoch in epoch array - if(idx == len - 1) { - // idx can be greater than actual length of epoch array by 1 - inflationRewardEpochBeginIdx[_operator] = epoch == currentEpoch ? idx : idx + 1; - } - - pendingInflationReward += Math.mulDiv(rewardPerEpoch, operatorJobCount[epoch][_operator], totalJobCount[epoch]); - } - - return pendingInflationReward; + return Math.mulDiv(inflationRewardPerEpoch, operatorJobCount[lastEpoch][_operator], totalJobCount[lastEpoch]); } - function _getPendingInflationReward(address _operator) internal view returns(uint256 pendingInflationReward) { - // check if operator has completed any job - if (operatorJobCompletionEpochs[_operator].length == 0) return 0; - - // list of epochs in which operator has completed jobs - uint256[] storage completedEpochs = operatorJobCompletionEpochs[_operator]; - - // first epoch which the reward has not been distributed - uint256 beginIdx = inflationRewardEpochBeginIdx[_operator]; - - // no job completed since last update - if(beginIdx > completedEpochs.length) return 0; - uint256 beginEpoch = completedEpochs[beginIdx]; - uint256 currentEpoch = block.timestamp / inflationRewardEpochSize; - // no pending reward if operator has already claimed reward until latest epoch - if(beginEpoch == currentEpoch) return 0; - - // update pending reward - uint256 rewardPerEpoch = inflationRewardPerEpoch; // cache - uint256 len = completedEpochs.length; - - for(uint256 idx = beginIdx; idx < len; idx++) { - uint256 epoch = completedEpochs[idx]; + /*======================================== Internal functions ========================================*/ - pendingInflationReward += Math.mulDiv(rewardPerEpoch, operatorJobCount[epoch][_operator], totalJobCount[epoch]); - } + function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { + // TODO: verify proof - return pendingInflationReward; + // TODO: emit event } /*======================================== Admin ========================================*/ From d1109cafc8f83895c0ca5231e8ac8aa2e7cca4ee Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 9 Oct 2024 16:01:24 +0900 Subject: [PATCH 142/158] add InflationRewardManager contract --- .../staking/IInflationRewardManager.sol | 6 + .../l2_contracts/InflationRewardManger.sol | 160 +++++++ contracts/staking/l2_contracts/JobManager.sol | 63 +-- .../l2_contracts/RewardDistributor.sol | 424 +++++++++--------- .../staking/l2_contracts/StakingManager.sol | 9 +- .../l2_contracts/SymbioticStakingReward.sol | 10 - 6 files changed, 388 insertions(+), 284 deletions(-) create mode 100644 contracts/interfaces/staking/IInflationRewardManager.sol create mode 100644 contracts/staking/l2_contracts/InflationRewardManger.sol diff --git a/contracts/interfaces/staking/IInflationRewardManager.sol b/contracts/interfaces/staking/IInflationRewardManager.sol new file mode 100644 index 0000000..4e5895b --- /dev/null +++ b/contracts/interfaces/staking/IInflationRewardManager.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +interface IInflationRewardManager { + function updatePendingInflationReward(address _operator) external returns (uint256 pendingInflationReward); +} \ No newline at end of file diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol new file mode 100644 index 0000000..eac5cf4 --- /dev/null +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +contract InflationRewardManager is + ContextUpgradeable, + ERC165Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + IInflationRewardManager +{ + address public jobManager; + address public stakingManager; + uint256 public startTime; + uint256 public inflationRewardEpochSize; + uint256 public inflationRewardPerEpoch; + + // last epoch when operator completed a job + mapping(address operator => uint256 lastJobCompletionEpoch) rewardEpoch; + + // TODO: temporary + mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% + + // count of jobs done by operator in an epoch + mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCountsPerEpoch; + + // total count of jobs done in an epoch + mapping(uint256 epoch => uint256 totalCount) totalJobCountsPerEpoch; + + modifier onlyJobManager() { + require(msg.sender == jobManager, "InflationRewardManager: Only JobManager"); + _; + } + + /*==================================================== initialize ===================================================*/ + + function initialize( + address _admin, + address _jobManager, + address _stakingManager, + uint256 _startTime, + uint256 _inflationRewardEpochSize, + uint256 _inflationRewardPerEpoch + ) public initializer { + __Context_init_unchained(); + __ERC165_init_unchained(); + __AccessControl_init_unchained(); + __UUPSUpgradeable_init_unchained(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + require(_jobManager != address(0), "InflationRewardManager: jobManager address is zero"); + jobManager = _jobManager; + + require(_stakingManager != address(0), "InflationRewardManager: stakingManager address is zero"); + stakingManager = _stakingManager; + + require(_startTime > 0, "InflationRewardManager: startTime is zero"); + startTime = _startTime; + + require(_inflationRewardEpochSize > 0, "InflationRewardManager: inflationRewardEpochSize is zero"); + inflationRewardEpochSize = _inflationRewardEpochSize; + + require(_inflationRewardPerEpoch > 0, "InflationRewardManager: inflationRewardPerEpoch is zero"); + inflationRewardPerEpoch = _inflationRewardPerEpoch; + } + + /*===================================================== external ====================================================*/ + + /// @notice update pending inflation reward for given operator + /// @dev called by JobManager when job is completed or by RewardDistributor when operator is slashed + function updatePendingInflationReward(address _operator) external returns (uint256 pendingInflationReward) { + uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; + uint256 operatorLastEpoch = rewardEpoch[_operator]; + + if (operatorLastEpoch == currentEpoch) { + return 0; + } + + if(msg.sender == jobManager) { + _increaseJobCount(_operator, currentEpoch); + } + + uint256 operatorLastEpochJobCount = operatorJobCountsPerEpoch[operatorLastEpoch][_operator]; + uint256 operatorCurrentEpochJobCount = operatorJobCountsPerEpoch[currentEpoch][_operator]; + + // when there is no job done by operator both in last epoch and current epoch don't update anything + if(operatorLastEpochJobCount * operatorCurrentEpochJobCount == 0) { + return 0; + } + + if(operatorLastEpochJobCount > 0) { + uint256 totalJobCount = totalJobCountsPerEpoch[operatorLastEpoch]; + + pendingInflationReward = Math.mulDiv( + inflationRewardPerEpoch, operatorLastEpochJobCount, totalJobCount + ); + + // when job is completed, inflation reward with distributed by JobManager along with fee reward + if(msg.sender != jobManager) { + IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); + } + } + + rewardEpoch[_operator] = currentEpoch; + } + + /*===================================================== internal ====================================================*/ + + function _increaseJobCount(address _operator, uint256 _epoch) internal { + operatorJobCountsPerEpoch[_epoch][_operator]++; + totalJobCountsPerEpoch[_epoch]++; + } + + /*======================================== Admin ========================================*/ + + function setJobManager(address _jobManager) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_jobManager != address(0), "InflationRewardManager: jobManager address is zero"); + jobManager = _jobManager; + } + + function setStakingManager(address _stakingManager) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_stakingManager != address(0), "InflationRewardManager: stakingManager address is zero"); + stakingManager = _stakingManager; + } + + function setInflationRewardPerEpoch(uint256 _inflationRewardPerEpoch) public onlyRole(DEFAULT_ADMIN_ROLE) { + inflationRewardPerEpoch = _inflationRewardPerEpoch; + } + + function setInflationRewardEpochSize(uint256 _inflationRewardEpochSize) public onlyRole(DEFAULT_ADMIN_ROLE) { + inflationRewardEpochSize = _inflationRewardEpochSize; + } + + /*======================================== Overrides ========================================*/ + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 22bec75..d66c5c9 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -10,6 +10,7 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {Struct} from "../../lib/staking/Struct.sol"; @@ -34,30 +35,13 @@ contract JobManager is uint256 public startTime; // TODO: immutable? + address public inflationRewardManager; address public stakingManager; address public feeToken; address public inflationRewardToken; uint256 public jobDuration; - uint256 inflationRewardEpochSize; - uint256 inflationRewardPerEpoch; - - // // epochs in which operator has done jobs - // mapping(address operator => uint256[] epochs) operatorJobCompletionEpochs; - // // idx of operatorJobCompletionEpochs, inflationReward distribution should be reflected from this idx - // mapping(address operator => uint256 idx) inflationRewardEpochBeginIdx; - - mapping(address operator => uint256 lastJobCompletionEpoch) opeartorLastJobCompletionEpochs; - - // TODO: temporary - mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% - - // count of jobs done by operator in an epoch - mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCount; - // total count of jobs done in an epoch - mapping(uint256 epoch => uint256 totalCount) totalJobCount; - /*======================================== Init ========================================*/ function initialize(uint256 _startTime, address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) @@ -109,12 +93,11 @@ contract JobManager is _verifyProof(_jobId, _proof); uint256 feePaid = jobs[_jobId].feePaid; - uint256 pendingInflationReward = updateInflationReward(jobs[_jobId].operator); // TODO: move to StakingManager - // send fee and unlock stake - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); + uint256 pendingInflationReward = IInflationRewardManager(inflationRewardManager).updatePendingInflationReward(jobs[_jobId].operator); - _updateJobCompletionEpoch(_jobId); + // distribute fee reward from the job and pending inflation reward + IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); } /** @@ -128,16 +111,6 @@ contract JobManager is uint256 jobId = _jobIds[idx]; submitProof(jobId, _proofs[idx]); // TODO: optimize - _updateJobCompletionEpoch(jobId); - } - } - - function _updateJobCompletionEpoch(uint256 _jobId) internal { - uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; - address operator = jobs[_jobId].operator; - - if(opeartorLastJobCompletionEpochs[operator] != currentEpoch) { - opeartorLastJobCompletionEpochs[operator] = currentEpoch; } } @@ -158,32 +131,6 @@ contract JobManager is } } - /*======================================== Inflation Reward ========================================*/ - - /// @notice update inflation reward for operator - /// @dev can be called by anyone, but most likely when proof is submitted(when job is completed) by operator - /// @dev or inflation reward is claimed in a RewardDistributor - function updateInflationReward(address _operator) public returns(uint256 pendingInflationReward) { - pendingInflationReward = getPendingInflationReward(_operator); - - if(pendingInflationReward > 0) { - // and distribute - IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); - } - } - - /// @notice update pending inflation reward for operator - function getPendingInflationReward(address _operator) public view returns(uint256 pendingInflationReward) { - uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; - uint256 lastEpoch = opeartorLastJobCompletionEpochs[_operator]; - - if(lastEpoch == currentEpoch) return 0; - - return Math.mulDiv(inflationRewardPerEpoch, operatorJobCount[lastEpoch][_operator], totalJobCount[lastEpoch]); - } - - - /*======================================== Internal functions ========================================*/ function _verifyProof(uint256 _jobId, bytes calldata _proof) internal { diff --git a/contracts/staking/l2_contracts/RewardDistributor.sol b/contracts/staking/l2_contracts/RewardDistributor.sol index 1e3c794..91f75b5 100644 --- a/contracts/staking/l2_contracts/RewardDistributor.sol +++ b/contracts/staking/l2_contracts/RewardDistributor.sol @@ -1,223 +1,221 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.26; - -import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; -import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; -import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - - -import {Struct} from "../../lib/staking/Struct.sol"; - -abstract contract RewardDistributor is - ContextUpgradeable, - ERC165Upgradeable, - AccessControlUpgradeable, - ReentrancyGuardUpgradeable, - PausableUpgradeable, - UUPSUpgradeable, - IRewardDistributor -{ - using Math for uint256; - using SafeERC20 for IERC20; - using EnumerableSet for EnumerableSet.AddressSet; - - address public jobManager; - address public stakingPool; - - address public feeRewardToken; - address public inflationRewardToken; - - // mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% - - // reward is accrued per operator - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) - rewards; - // rewardTokens amount per stakeToken - mapping( - address stakeToken - => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken)) - ) rewardPerTokenStored; - - mapping( - address account - => mapping( - address stakeToken - => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)) - ) - ) rewardPerTokenPaids; - - mapping(address account => mapping(address rewardToken => uint256 amount)) rewardAccrued; - - modifier onlyStakingPool() { - require(msg.sender == stakingPool, "Only StakingPool"); - _; - } - - /*============================================= init =============================================*/ - - function initialize( - address _admin, - address _jobManager, - address _stakingPool, - address _feeRewardToken, - address _inflationRewardToken - ) public initializer { - __Context_init_unchained(); - __ERC165_init_unchained(); - __AccessControl_init_unchained(); - __UUPSUpgradeable_init_unchained(); - __ReentrancyGuard_init_unchained(); - __ReentrancyGuard_init_unchained(); - - _grantRole(DEFAULT_ADMIN_ROLE, _admin); - - require(_admin != address(0), "Invalid Admin"); - require(_jobManager != address(0), "Invalid JobManager"); - require(_stakingPool != address(0), "Invalid StakingPool"); - require(_feeRewardToken != address(0), "Invalid FeeRewardToken"); - - jobManager = _jobManager; - stakingPool = _stakingPool; - feeRewardToken = _feeRewardToken; - inflationRewardToken = _inflationRewardToken; - } - - /*======================================== external functions ========================================*/ - - /// @notice called when fee reward is generated - function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlyStakingPool { - rewards[_stakeToken][_operator][feeRewardToken] += _rewardAmount; - rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); - } - - /// @notice called when inflation reward is generated - function updateInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingPool { - address[] memory stakeTokenList = _getStakeTokenList(); - for(uint256 i = 0; i < stakeTokenList.length; i++) { - rewards[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount; - rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); - } - } - - // /// @dev called when stake amount is updated in StakingPool - // function onStakeUpdate(address _account, address _stakeToken, address _operator) external onlyStakingPool { - // // update fee reward - // rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); - - // // update inflation reward - // // TODO: check if there is any problem by not updating rewardPerTokenStored during Tx - // _requestInflationRewardUpdate(_operator); - - // uint256 rewardPerTokenStoredCurrent = rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken]; - // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); +// // SPDX-License-Identifier: MIT + +// pragma solidity ^0.8.26; + +// import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +// import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +// import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +// import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +// import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +// import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; +// import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +// import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; + +// import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +// import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + + +// import {Struct} from "../../lib/staking/Struct.sol"; + +// abstract contract RewardDistributor is +// ContextUpgradeable, +// ERC165Upgradeable, +// AccessControlUpgradeable, +// ReentrancyGuardUpgradeable, +// PausableUpgradeable, +// UUPSUpgradeable, +// IRewardDistributor +// { +// using Math for uint256; +// using SafeERC20 for IERC20; +// using EnumerableSet for EnumerableSet.AddressSet; + +// address public jobManager; +// address public stakingPool; + +// address public feeRewardToken; +// address public inflationRewardToken; + +// // mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% + +// // reward is accrued per operator +// mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardAmount))) +// rewards; +// // rewardTokens amount per stakeToken +// mapping( +// address stakeToken +// => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken)) +// ) rewardPerTokenStored; + +// mapping( +// address account +// => mapping( +// address stakeToken +// => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)) +// ) +// ) rewardPerTokenPaids; + +// mapping(address account => mapping(address rewardToken => uint256 amount)) rewardAccrued; + +// modifier onlyStakingPool() { +// require(msg.sender == stakingPool, "Only StakingPool"); +// _; +// } + +// /*============================================= init =============================================*/ + +// function initialize( +// address _admin, +// address _jobManager, +// address _stakingPool, +// address _feeRewardToken, +// address _inflationRewardToken +// ) public initializer { +// __Context_init_unchained(); +// __ERC165_init_unchained(); +// __AccessControl_init_unchained(); +// __UUPSUpgradeable_init_unchained(); +// __ReentrancyGuard_init_unchained(); +// __ReentrancyGuard_init_unchained(); + +// _grantRole(DEFAULT_ADMIN_ROLE, _admin); + +// require(_admin != address(0), "Invalid Admin"); +// require(_jobManager != address(0), "Invalid JobManager"); +// require(_stakingPool != address(0), "Invalid StakingPool"); +// require(_feeRewardToken != address(0), "Invalid FeeRewardToken"); + +// jobManager = _jobManager; +// stakingPool = _stakingPool; +// feeRewardToken = _feeRewardToken; +// inflationRewardToken = _inflationRewardToken; +// } + +// /*======================================== external functions ========================================*/ + +// /// @notice called when fee reward is generated +// function updateFeeReward(address _stakeToken, address _operator, uint256 _rewardAmount) external onlyStakingPool { +// rewards[_stakeToken][_operator][feeRewardToken] += _rewardAmount; +// rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); +// } + +// /// @notice called when inflation reward is generated +// function updateInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingPool { +// address[] memory stakeTokenList = _getStakeTokenList(); +// for(uint256 i = 0; i < stakeTokenList.length; i++) { +// rewards[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount; +// rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); +// } +// } + +// // /// @dev called when stake amount is updated in StakingPool +// // function onStakeUpdate(address _account, address _stakeToken, address _operator) external onlyStakingPool { +// // // update fee reward +// // rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] = _rewardPerTokenStored(_stakeToken, _operator, feeRewardToken); + +// // // update inflation reward +// // // TODO: check if there is any problem by not updating rewardPerTokenStored during Tx +// // _requestInflationRewardUpdate(_operator); + +// // uint256 rewardPerTokenStoredCurrent = rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken]; +// // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); - // for(uint256 i = 0; i < stakeTokenList.length; i++) { - // uint256 rewardPerTokenPaid = rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken]; - // uint256 accountStakeAmount = _getStakeAmount(_account, stakeTokenList[i], _operator); - // uint256 pendingReward = accountStakeAmount.mulDiv(rewardPerTokenStoredCurrent - rewardPerTokenPaid, 1e18); - - // // update account's reward info - // rewardAccrued[_account][inflationRewardToken] += pendingReward; - // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStoredCurrent; - - // // update global rewardPerTokenStored - // uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, stakeTokenList[i]); - // rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken] += rewards[stakeTokenList[i]][_operator][inflationRewardToken].mulDiv(1e18, operatorStakeAmount); - // } - // } - - // function onClaimReward(address _account, address _operator) external onlyStakingPool { - // IERC20(feeRewardToken).safeTransfer(_account, rewardAccrued[_account][feeRewardToken]); - // IERC20(inflationRewardToken).safeTransfer(_account, rewardAccrued[_account][inflationRewardToken]); - - // rewardAccrued[_account][feeRewardToken] = 0; - // rewardAccrued[_account][inflationRewardToken] = 0; - - // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); - // for(uint256 i = 0; i < stakeTokenList.length; i++) { - // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][feeRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][feeRewardToken]; - // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken]; - // } - // } - - - - function onSlash() external onlyStakingPool { - // TODO - } - - /*======================================== internal functions ========================================*/ - function _requestInflationRewardUpdate(address _operator) internal { - // JobManager.updateInflationReward - // -> StakingManager.distributeInflationReward - // -> StakingPool.distributeInflationReward - // -> RewardDistributor.addInflationReward - IJobManager(jobManager).updateInflationReward(_operator); - } - - /* Modification for _update */ - function _rewardPerTokenStored(address _stakeToken, address _operator, address _rewardToken, uint256 rewardAmount) - internal - view - returns (uint256) - { - uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); - if(operatorStakeAmount == 0) return rewardPerTokenStored[_stakeToken][_operator][_rewardToken]; - - return rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + rewardAmount.mulDiv(1e18, operatorStakeAmount); - } - - function _updatePendingInflationReward(address _operator) internal { - - } - - - /*======================================== internal view functions ========================================*/ - function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { - return IStakingPool(stakingPool).getOperatorStakeAmount(_operator, _stakeToken); - } +// // for(uint256 i = 0; i < stakeTokenList.length; i++) { +// // uint256 rewardPerTokenPaid = rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken]; +// // uint256 accountStakeAmount = _getStakeAmount(_account, stakeTokenList[i], _operator); +// // uint256 pendingReward = accountStakeAmount.mulDiv(rewardPerTokenStoredCurrent - rewardPerTokenPaid, 1e18); + +// // // update account's reward info +// // rewardAccrued[_account][inflationRewardToken] += pendingReward; +// // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStoredCurrent; + +// // // update global rewardPerTokenStored +// // uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, stakeTokenList[i]); +// // rewardPerTokenStored[_stakeToken][_operator][inflationRewardToken] += rewards[stakeTokenList[i]][_operator][inflationRewardToken].mulDiv(1e18, operatorStakeAmount); +// // } +// // } + +// // function onClaimReward(address _account, address _operator) external onlyStakingPool { +// // IERC20(feeRewardToken).safeTransfer(_account, rewardAccrued[_account][feeRewardToken]); +// // IERC20(inflationRewardToken).safeTransfer(_account, rewardAccrued[_account][inflationRewardToken]); + +// // rewardAccrued[_account][feeRewardToken] = 0; +// // rewardAccrued[_account][inflationRewardToken] = 0; + +// // address[] memory stakeTokenList = IStakingPool(stakingPool).getStakeTokenList(); +// // for(uint256 i = 0; i < stakeTokenList.length; i++) { +// // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][feeRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][feeRewardToken]; +// // rewardPerTokenPaids[_account][stakeTokenList[i]][_operator][inflationRewardToken] = rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken]; +// // } +// // } + +// function onSlash() external onlyStakingPool { +// // TODO +// } + +// /*======================================== internal functions ========================================*/ +// function _requestInflationRewardUpdate(address _operator) internal { +// // JobManager.updateInflationReward +// // -> StakingManager.distributeInflationReward +// // -> StakingPool.distributeInflationReward +// // -> RewardDistributor.addInflationReward +// IJobManager(jobManager).updateInflationReward(_operator); +// } + +// /* Modification for _update */ +// function _rewardPerTokenStored(address _stakeToken, address _operator, address _rewardToken, uint256 rewardAmount) +// internal +// view +// returns (uint256) +// { +// uint256 operatorStakeAmount = _getOperatorStakeAmount(_operator, _stakeToken); +// if(operatorStakeAmount == 0) return rewardPerTokenStored[_stakeToken][_operator][_rewardToken]; + +// return rewardPerTokenStored[_stakeToken][_operator][_rewardToken] + rewardAmount.mulDiv(1e18, operatorStakeAmount); +// } + +// function _updatePendingInflationReward(address _operator) internal { + +// } + + +// /*======================================== internal view functions ========================================*/ +// function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { +// return IStakingPool(stakingPool).getOperatorStakeAmount(_operator, _stakeToken); +// } - function _getStakeTokenList() internal view returns (address[] memory) { - return IStakingPool(stakingPool).getStakeTokenList(); - } +// function _getStakeTokenList() internal view returns (address[] memory) { +// return IStakingPool(stakingPool).getStakeTokenList(); +// } - function _getStakeAmount(address account, address _stakeToken, address _operator) internal view returns (uint256) { - return IStakingPool(stakingPool).getStakeAmount(account, _stakeToken, _operator); - } +// function _getStakeAmount(address account, address _stakeToken, address _operator) internal view returns (uint256) { +// return IStakingPool(stakingPool).getStakeAmount(account, _stakeToken, _operator); +// } - /*======================================== admin functions ========================================*/ +// /*======================================== admin functions ========================================*/ - function setStakingPool(address _stakingPool) public onlyRole(DEFAULT_ADMIN_ROLE) { - stakingPool = _stakingPool; - // TODO: emit event - } +// function setStakingPool(address _stakingPool) public onlyRole(DEFAULT_ADMIN_ROLE) { +// stakingPool = _stakingPool; +// // TODO: emit event +// } - /*======================================== overrides ========================================*/ +// /*======================================== overrides ========================================*/ - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC165Upgradeable, AccessControlUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId); - } +// function supportsInterface(bytes4 interfaceId) +// public +// view +// virtual +// override(ERC165Upgradeable, AccessControlUpgradeable) +// returns (bool) +// { +// return super.supportsInterface(interfaceId); +// } - function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} +// function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} -} +// } diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 0c25919..2f6f6f7 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -43,6 +43,11 @@ contract StakingManager is _; } + modifier onlyInflationRewardManager() { + require(msg.sender == jobManager, "StakingManager: Only JobManager"); + _; + } + function initialize(address _admin, address _jobManager) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); @@ -101,7 +106,7 @@ contract StakingManager is } } - function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyJobManager { + function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyInflationRewardManager { if(_rewardAmount == 0) return; uint256 len = stakingPoolSet.length(); @@ -109,9 +114,7 @@ contract StakingManager is address pool = stakingPoolSet.at(i); (, uint256 poolRewardAmount) = _calcRewardAmount(pool, 0, _rewardAmount); - IERC20(inflationRewardToken).safeTransfer(pool, poolRewardAmount); - // TODO IStakingPool(pool).distributeInflationReward(_operator, poolRewardAmount); } } diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 9963b75..f036a06 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -128,8 +128,6 @@ contract SymbioticStakingReward is // update pending inflation reward and update rewardPerToken for each operator and vault // feeReward is updated on job completion so no need to update here for (uint256 i = 0; i < _vaultSnapshots.length; i++) { - // update pending inflation reward of the operator and update rewardPerTokenStored - _updatePendingInflationReward(_vaultSnapshots[i].operator); // update rewardPerTokenPaid and rewardAccrued for each vault _updateVaultInflationReward(stakeTokenList, _vaultSnapshots[i].vault, _vaultSnapshots[i].operator); @@ -140,9 +138,6 @@ contract SymbioticStakingReward is /// @notice vault can claim reward calling this function function claimReward(address _operator) external nonReentrant { - // update pending inflation reward of the operator and update rewardPerTokenStored - _updatePendingInflationReward(_operator); - // update rewardPerTokenPaid and rewardAccrued for each vault _updateVaultInflationReward(_getStakeTokenList(), _msgSender(), _operator); @@ -165,11 +160,6 @@ contract SymbioticStakingReward is /*===================================================== internal ====================================================*/ - /// @dev update pending inflation reward and update rewardPerTokenStored of the operator - function _updatePendingInflationReward(address _operator) internal { - IJobManager(jobManager).updateInflationReward(_operator); - } - /// @dev update rewardPerToken and rewardAccrued for each vault function _updateVaultInflationReward(address[] memory _stakeTokenList, address _vault, address _operator) internal From 106fc42e4da9109515428c6584ad2047e37f7699 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Wed, 9 Oct 2024 16:39:19 +0900 Subject: [PATCH 143/158] send comission to operator --- .../l2_contracts/InflationRewardManger.sol | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index eac5cf4..ad750a1 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -12,6 +12,8 @@ import {IInflationRewardManager} from "../../interfaces/staking/IInflationReward import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract InflationRewardManager is ContextUpgradeable, @@ -21,12 +23,21 @@ contract InflationRewardManager is ReentrancyGuardUpgradeable, IInflationRewardManager { + using SafeERC20 for IERC20; + + uint256 public startTime; + address public jobManager; address public stakingManager; - uint256 public startTime; + + address public inflationRewardToken; + uint256 public inflationRewardEpochSize; uint256 public inflationRewardPerEpoch; + // operator deducts comission from inflation reward + mapping(address operator => uint256 rewardShare) operatorRewardShare; // 1e18 == 100% + // last epoch when operator completed a job mapping(address operator => uint256 lastJobCompletionEpoch) rewardEpoch; @@ -48,9 +59,10 @@ contract InflationRewardManager is function initialize( address _admin, + uint256 _startTime, address _jobManager, address _stakingManager, - uint256 _startTime, + address _inflationRewardToken, uint256 _inflationRewardEpochSize, uint256 _inflationRewardPerEpoch ) public initializer { @@ -70,6 +82,9 @@ contract InflationRewardManager is require(_startTime > 0, "InflationRewardManager: startTime is zero"); startTime = _startTime; + require(_inflationRewardToken != address(0), "InflationRewardManager: inflationRewardToken address is zero"); + inflationRewardToken = _inflationRewardToken; + require(_inflationRewardEpochSize > 0, "InflationRewardManager: inflationRewardEpochSize is zero"); inflationRewardEpochSize = _inflationRewardEpochSize; @@ -101,17 +116,28 @@ contract InflationRewardManager is return 0; } + // when operator has done job in last epoch, distribute inflation reward + // if 0, it means pendingInflationReward was updated and no job has been done if(operatorLastEpochJobCount > 0) { uint256 totalJobCount = totalJobCountsPerEpoch[operatorLastEpoch]; - + pendingInflationReward = Math.mulDiv( inflationRewardPerEpoch, operatorLastEpochJobCount, totalJobCount ); - // when job is completed, inflation reward with distributed by JobManager along with fee reward - if(msg.sender != jobManager) { - IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); - } + // operator deducts comission from inflation reward + uint256 operatorComission = Math.mulDiv( + pendingInflationReward, operatorRewardShare[_operator], 1e18 + ); + IERC20(inflationRewardToken).safeTransfer(_operator, pendingInflationReward - operatorComission); + + pendingInflationReward -= operatorComission; + } + + // when job is completed, inflation reward with distributed by JobManager along with fee reward + if(msg.sender != jobManager && pendingInflationReward > 0) { + // staking manager will distribute inflation reward based on each pool's share + IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); } rewardEpoch[_operator] = currentEpoch; From 82442e3732f3ae79849253482aa6abefbffcdeaa Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 10 Oct 2024 11:12:43 +0900 Subject: [PATCH 144/158] add interaction with InflationRewardManager --- .../staking/l2_contracts/SymbioticStaking.sol | 3 -- .../l2_contracts/SymbioticStakingReward.sol | 50 ++++++++----------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index b525157..ce61112 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -167,7 +167,6 @@ contract SymbioticStaking is // Store transmitter address to reward when job is closed uint256 timestampIdx = confirmedTimestamps.length - 1; - address transmitter = confirmedTimestamps[timestampIdx].transmitter; lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], timestampIdx); operatorLockedAmounts[_operator][_token] += amountToLock[_token]; @@ -266,8 +265,6 @@ contract SymbioticStaking is // TODO: emit event for each update? } - - ISymbioticStakingReward(rewardDistributor).onSnapshotSubmission(_vaultSnapshots); } function _completeSubmission(uint256 _captureTimestamp) internal { diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index f036a06..488f908 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -11,8 +11,9 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -32,17 +33,11 @@ contract SymbioticStakingReward is address public jobManager; address public symbioticStaking; + address public inflationRewardManager; address public feeRewardToken; address public inflationRewardToken; - /* - rewardToken: Fee Reward, Inflation Reward - stakeToken: staking token - */ - - // reward accrued per operator - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 amount))) rewards; // TODO: check if needed // rewardTokens amount per stakeToken mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) @@ -69,6 +64,7 @@ contract SymbioticStakingReward is function initialize( address _admin, + address _inflationRewardManager, address _jobManager, address _symbioticStaking, address _feeRewardToken, @@ -83,10 +79,19 @@ contract SymbioticStakingReward is _grantRole(DEFAULT_ADMIN_ROLE, _admin); - jobManager = _jobManager; + require(_inflationRewardManager != address(0), "SymbioticStakingReward: inflationRewardManager address is zero"); + inflationRewardManager = _inflationRewardManager; + + require(_jobManager != address(0), "SymbioticStakingReward: jobManager address is zero"); + jobManager = _jobManager; + + require(_symbioticStaking != address(0), "SymbioticStakingReward: symbioticStaking address is zero"); symbioticStaking = _symbioticStaking; + require(_feeRewardToken != address(0), "SymbioticStakingReward: feeRewardToken address is zero"); feeRewardToken = _feeRewardToken; + + require(_inflationRewardToken != address(0), "SymbioticStakingReward: inflationRewardToken address is zero"); inflationRewardToken = _inflationRewardToken; } @@ -102,7 +107,6 @@ contract SymbioticStakingReward is { rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); - IERC20(feeRewardToken).safeTransferFrom(jobManager, address(this), _rewardAmount); } /// @notice called when inflation reward is generated @@ -113,31 +117,16 @@ contract SymbioticStakingReward is rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); } - IERC20(inflationRewardToken).safeTransferFrom(jobManager, address(this), _rewardAmount); } - /* ------------------------- symbiotic staking ------------------------- */ - - /// @notice update rewardPerToken and rewardAccrued for each vault - /// @dev called when snapshot is submitted - function onSnapshotSubmission(Struct.VaultSnapshot[] calldata _vaultSnapshots) external onlySymbioticStaking { - // TODO: this can be called redundantly as each snapshots can include same operator and vault address - // update inlfationReward info of each vault - address[] memory stakeTokenList = _getStakeTokenList(); - - // update pending inflation reward and update rewardPerToken for each operator and vault - // feeReward is updated on job completion so no need to update here - for (uint256 i = 0; i < _vaultSnapshots.length; i++) { - - // update rewardPerTokenPaid and rewardAccrued for each vault - _updateVaultInflationReward(stakeTokenList, _vaultSnapshots[i].vault, _vaultSnapshots[i].operator); - } - } /* ------------------------- reward claim ------------------------- */ /// @notice vault can claim reward calling this function function claimReward(address _operator) external nonReentrant { + // update pending inflation reward for the operator + _updatePendingInflaionReward(_operator); + // update rewardPerTokenPaid and rewardAccrued for each vault _updateVaultInflationReward(_getStakeTokenList(), _msgSender(), _operator); @@ -160,6 +149,11 @@ contract SymbioticStakingReward is /*===================================================== internal ====================================================*/ + /// @dev this will update pending inflation reward and rewardPerToken for the operator + function _updatePendingInflaionReward(address _operator) internal { + IInflationRewardManager(inflationRewardManager).updatePendingInflationReward(_operator); + } + /// @dev update rewardPerToken and rewardAccrued for each vault function _updateVaultInflationReward(address[] memory _stakeTokenList, address _vault, address _operator) internal From 7ef70c5e4f216a4a844cad5db52ae9253aa7b950 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 10 Oct 2024 17:18:14 +0900 Subject: [PATCH 145/158] add transmitter comission feature --- .../staking/IInflationRewardManager.sol | 4 +- contracts/interfaces/staking/IJobManager.sol | 2 +- .../interfaces/staking/IStakingManager.sol | 2 +- contracts/interfaces/staking/IStakingPool.sol | 2 +- .../interfaces/staking/ISymbioticStaking.sol | 7 +- contracts/lib/staking/Struct.sol | 1 - .../l2_contracts/InflationRewardManger.sol | 72 ++++++++++++----- contracts/staking/l2_contracts/JobManager.sol | 37 ++++++--- .../staking/l2_contracts/NativeStaking.sol | 3 +- .../staking/l2_contracts/StakingManager.sol | 25 +++++- .../staking/l2_contracts/SymbioticStaking.sol | 77 +++++++++++++------ .../l2_contracts/SymbioticStakingReward.sol | 1 - 12 files changed, 168 insertions(+), 65 deletions(-) diff --git a/contracts/interfaces/staking/IInflationRewardManager.sol b/contracts/interfaces/staking/IInflationRewardManager.sol index 4e5895b..8dab1f5 100644 --- a/contracts/interfaces/staking/IInflationRewardManager.sol +++ b/contracts/interfaces/staking/IInflationRewardManager.sol @@ -2,5 +2,7 @@ pragma solidity ^0.8.26; interface IInflationRewardManager { - function updatePendingInflationReward(address _operator) external returns (uint256 pendingInflationReward); + function updatePendingInflationReward(address _operator) external returns (uint256 timestampIdx, uint256 pendingInflationReward); + + function updateEpochTimestampIdx() external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IJobManager.sol b/contracts/interfaces/staking/IJobManager.sol index 26e9237..b990d73 100644 --- a/contracts/interfaces/staking/IJobManager.sol +++ b/contracts/interfaces/staking/IJobManager.sol @@ -8,5 +8,5 @@ interface IJobManager { function refundFee(uint256 jobId) external; - // function updateInflationReward(address _operator) external; + function operatorRewardShares(address _operator) external view returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index f78a208..3818a4f 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.26; interface IStakingManager { function onJobCreation(uint256 jobId, address operator) external; - function onJobCompletion(uint256 jobId, address operator, uint256 feePaid, uint256 inflationReward) external; + function onJobCompletion(uint256 jobId, address operator, uint256 feePaid) external; function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index 23a6dcc..60287e4 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -8,7 +8,7 @@ interface IStakingPool { function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external; // Staking Manager only + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount, uint256 _timestampIdx) external; // Staking Manager only function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index a108784..ec78e83 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -15,5 +15,8 @@ interface ISymbioticStaking is IStakingPool { // event SubmissionCompleted /// @notice Returns the captureTimestamp of latest completed snapshot submission - function lastConfirmedTimestamp() external view returns (uint256); -} + function latestConfirmedTimestamp() external view returns (uint256); + + /// @notice Returns the timestampIdx of latest completed snapshot submission + function latestConfirmedTimestampIdx() external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 700717a..1519d95 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -68,6 +68,5 @@ library Struct { struct SymbioticStakingLock { address stakeToken; uint256 amount; - uint256 timestampIdx; } } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index ad750a1..131d17a 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -9,6 +9,7 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/ut import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -25,36 +26,42 @@ contract InflationRewardManager is { using SafeERC20 for IERC20; + /* config */ uint256 public startTime; + /* contract addresses */ address public jobManager; address public stakingManager; + address public symbioticStaking; + /* reward config */ address public inflationRewardToken; - uint256 public inflationRewardEpochSize; uint256 public inflationRewardPerEpoch; - // operator deducts comission from inflation reward - mapping(address operator => uint256 rewardShare) operatorRewardShare; // 1e18 == 100% - // last epoch when operator completed a job - mapping(address operator => uint256 lastJobCompletionEpoch) rewardEpoch; + mapping(address operator => uint256 lastJobCompletionEpoch) lastJobCompletionEpochs; // TODO: temporary mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% // count of jobs done by operator in an epoch mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCountsPerEpoch; - // total count of jobs done in an epoch mapping(uint256 epoch => uint256 totalCount) totalJobCountsPerEpoch; + // timestampIdx of the latestConfirmedTimestamp at the time of job completion or snapshot submission + mapping(uint256 epoch => uint256 timestampIdx) epochTimestampIdx; modifier onlyJobManager() { require(msg.sender == jobManager, "InflationRewardManager: Only JobManager"); _; } + modifier onlySymbioticStaking() { + require(msg.sender == symbioticStaking, "InflationRewardManager: Only SymbioticStaking"); + _; + } + /*==================================================== initialize ===================================================*/ function initialize( @@ -96,51 +103,66 @@ contract InflationRewardManager is /// @notice update pending inflation reward for given operator /// @dev called by JobManager when job is completed or by RewardDistributor when operator is slashed - function updatePendingInflationReward(address _operator) external returns (uint256 pendingInflationReward) { + function updatePendingInflationReward(address _operator) external returns (uint256 timestampIdx, uint256 pendingInflationReward) { uint256 currentEpoch = (block.timestamp - startTime) / inflationRewardEpochSize; - uint256 operatorLastEpoch = rewardEpoch[_operator]; + uint256 operatorLastEpoch = lastJobCompletionEpochs[_operator]; + // no need to update and distribute pending inflation reward if (operatorLastEpoch == currentEpoch) { - return 0; + // return address(0) as the transmitter value will not be used + return (0, 0); } - if(msg.sender == jobManager) { + // when job is completed, increase job count + if(msg.sender == stakingManager) { _increaseJobCount(_operator, currentEpoch); } uint256 operatorLastEpochJobCount = operatorJobCountsPerEpoch[operatorLastEpoch][_operator]; uint256 operatorCurrentEpochJobCount = operatorJobCountsPerEpoch[currentEpoch][_operator]; - // when there is no job done by operator both in last epoch and current epoch don't update anything - if(operatorLastEpochJobCount * operatorCurrentEpochJobCount == 0) { - return 0; + // if operator has not completed any job + if(operatorLastEpochJobCount == 0 && operatorCurrentEpochJobCount == 0) { + // return address(0) as the transmitter value will not be used + return (0, 0); } // when operator has done job in last epoch, distribute inflation reward // if 0, it means pendingInflationReward was updated and no job has been done if(operatorLastEpochJobCount > 0) { - uint256 totalJobCount = totalJobCountsPerEpoch[operatorLastEpoch]; + uint256 lastEpochTotalJobCount = totalJobCountsPerEpoch[operatorLastEpoch]; pendingInflationReward = Math.mulDiv( - inflationRewardPerEpoch, operatorLastEpochJobCount, totalJobCount + inflationRewardPerEpoch, operatorLastEpochJobCount, lastEpochTotalJobCount ); // operator deducts comission from inflation reward uint256 operatorComission = Math.mulDiv( - pendingInflationReward, operatorRewardShare[_operator], 1e18 + pendingInflationReward, IJobManager(jobManager).operatorRewardShares(_operator), 1e18 ); - IERC20(inflationRewardToken).safeTransfer(_operator, pendingInflationReward - operatorComission); + + IERC20(inflationRewardToken).safeTransfer(_operator, operatorComission); pendingInflationReward -= operatorComission; } // when job is completed, inflation reward with distributed by JobManager along with fee reward - if(msg.sender != jobManager && pendingInflationReward > 0) { + if(msg.sender != stakingManager && pendingInflationReward > 0) { // staking manager will distribute inflation reward based on each pool's share IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); } - rewardEpoch[_operator] = currentEpoch; + lastJobCompletionEpochs[_operator] = currentEpoch; + } + + /// @notice update when snapshot submission is completed, or when a job is completed + function updateEpochTimestampIdx() external onlySymbioticStaking { + // latest confirmed timestampIdx + uint256 currentTimestampIdx = ISymbioticStaking(symbioticStaking).latestConfirmedTimestampIdx(); + + if(epochTimestampIdx[_getCurrentEpoch()] != currentTimestampIdx) { + epochTimestampIdx[_getCurrentEpoch()] = currentTimestampIdx; + } } /*===================================================== internal ====================================================*/ @@ -150,6 +172,18 @@ contract InflationRewardManager is totalJobCountsPerEpoch[_epoch]++; } + /*=================================================== external view =================================================*/ + + function getEpochTimestampIdx(uint256 _epoch) external view returns (uint256) { + return epochTimestampIdx[_epoch]; + } + + /*=================================================== internal view =================================================*/ + + function _getCurrentEpoch() internal view returns (uint256) { + return (block.timestamp - startTime) / inflationRewardEpochSize; + } + /*======================================== Admin ========================================*/ function setJobManager(address _jobManager) public onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index d66c5c9..fb9b369 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -32,19 +32,20 @@ contract JobManager is using SafeERC20 for IERC20; mapping(uint256 jobId => Struct.JobInfo jobInfo) public jobs; + // operator deducts comission from inflation reward + mapping(address operator => uint256 rewardShare) public operatorRewardShares; // 1e18 == 100% uint256 public startTime; // TODO: immutable? - address public inflationRewardManager; address public stakingManager; address public feeToken; - address public inflationRewardToken; + address public inflationRewardManager; uint256 public jobDuration; /*======================================== Init ========================================*/ - function initialize(uint256 _startTime, address _admin, address _stakingManager, address _feeToken, uint256 _jobDuration) + function initialize(uint256 _startTime, address _admin, address _stakingManager, address _feeToken, address _inflationRewardManager, uint256 _jobDuration) public initializer { @@ -55,9 +56,19 @@ contract JobManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); + require(_startTime >= block.timestamp, "JobManager: Invalid Start Time"); startTime = _startTime; + + require(_stakingManager != address(0), "JobManager: Invalid StakingManager"); stakingManager = _stakingManager; + + require(_feeToken != address(0), "JobManager: Invalid Fee Token"); feeToken = _feeToken; + + require(_inflationRewardManager != address(0), "JobManager: Invalid InflationRewardManager"); + inflationRewardManager = _inflationRewardManager; + + require(_jobDuration > 0, "JobManager: Invalid Job Duration"); jobDuration = _jobDuration; } @@ -91,13 +102,14 @@ contract JobManager is require(block.timestamp <= jobs[_jobId].deadline, "Job Expired"); _verifyProof(_jobId, _proof); + + address operator = jobs[_jobId].operator; - uint256 feePaid = jobs[_jobId].feePaid; - - uint256 pendingInflationReward = IInflationRewardManager(inflationRewardManager).updatePendingInflationReward(jobs[_jobId].operator); + // distribute fee reward + uint256 feeRewardRemaining = _distributeFeeReward(operator, jobs[_jobId].feePaid); - // distribute fee reward from the job and pending inflation reward - IStakingManager(stakingManager).onJobCompletion(_jobId, jobs[_jobId].operator, feePaid, pendingInflationReward); + // inflation reward will be distributed here + IStakingManager(stakingManager).onJobCompletion(_jobId, operator, feeRewardRemaining); } /** @@ -109,8 +121,7 @@ contract JobManager is uint256 len = _jobIds.length; for (uint256 idx = 0; idx < len; idx++) { uint256 jobId = _jobIds[idx]; - submitProof(jobId, _proofs[idx]); // TODO: optimize - + submitProof(jobId, _proofs[idx]); } } @@ -139,6 +150,12 @@ contract JobManager is // TODO: emit event } + function _distributeFeeReward(address _operator, uint256 _feePaid) internal returns(uint256 feeRewardRemaining) { + uint256 operatorFeeReward = Math.mulDiv(_feePaid, operatorRewardShares[_operator], 1e18); + IERC20(feeToken).safeTransfer(_operator, operatorFeeReward); + feeRewardRemaining = _feePaid - operatorFeeReward; + } + /*======================================== Admin ========================================*/ function setStakingManager(address _stakingManager) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 2654e60..4fce612 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -157,7 +157,8 @@ contract NativeStaking is uint256 _jobId, address _operator, uint256 _feeRewardAmount, - uint256 _inflationRewardAmount + uint256 _inflationRewardAmount, + uint256 /* _inflationRewardTimestampIdx */ ) external onlyStakingManager { Struct.NativeStakingLock memory lock = lockInfo[_jobId]; diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 2f6f6f7..bb7f84d 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -14,6 +14,7 @@ import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -32,6 +33,8 @@ contract StakingManager is EnumerableSet.AddressSet private stakingPoolSet; address public jobManager; + address public inflationRewardManager; + address public feeToken; address public inflationRewardToken; @@ -48,7 +51,7 @@ contract StakingManager is _; } - function initialize(address _admin, address _jobManager) public initializer { + function initialize(address _admin, address _jobManager, address _inflationRewardManager, address _feeToken, address _inflationRewardToken) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -56,7 +59,17 @@ contract StakingManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); + require(_jobManager != address(0), "StakingManager: Invalid JobManager"); jobManager = _jobManager; + + require(_inflationRewardManager != address(0), "StakingManager: Invalid InflationRewardManager"); + inflationRewardManager = _inflationRewardManager; + + require(_feeToken != address(0), "StakingManager: Invalid FeeToken"); + feeToken = _feeToken; + + require(_inflationRewardToken != address(0), "StakingManager: Invalid InflationRewardToken"); + inflationRewardToken = _inflationRewardToken; } // create job and lock stakes (operator self stake, some portion of native stake and symbiotic stake) @@ -75,15 +88,19 @@ contract StakingManager is } // called when job is completed to unlock the locked stakes - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feePaid, uint256 _inflationRewardAmount) external onlyJobManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount) external onlyJobManager { + // update pending inflation reward + (uint256 timestampIdx, uint256 pendingInflationReward) = IInflationRewardManager(inflationRewardManager).updatePendingInflationReward(_operator); + uint256 len = stakingPoolSet.length(); for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); - (uint256 poolFeeRewardAmount, uint256 poolInflationRewardAmount) = _calcRewardAmount(pool, _feePaid, _inflationRewardAmount); + (uint256 poolFeeRewardAmount, uint256 poolInflationRewardAmount) = _calcRewardAmount(pool, _feeRewardAmount, pendingInflationReward); - IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, poolInflationRewardAmount); + IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, poolInflationRewardAmount, timestampIdx); } + // TODO: emit event } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index ce61112..297ac88 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -18,7 +18,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; - +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; contract SymbioticStaking is ContextUpgradeable, ERC165Upgradeable, @@ -43,10 +43,13 @@ contract SymbioticStaking is EnumerableSet.AddressSet stakeTokenSet; - address public feeRewardToken; - address public inflationRewardToken; address public stakingManager; address public rewardDistributor; + address public inflationRewardManager; + + address public feeRewardToken; + address public inflationRewardToken; + /* Config */ mapping(address token => uint256 amount) public amountToLock; @@ -77,7 +80,7 @@ contract SymbioticStaking is _; } - function initialize(address _admin, address _stakingManager, address _rewardDistributor) public initializer { + function initialize(address _admin, address _stakingManager, address _rewardDistributor, address _inflationRewardManager, address _feeRewardToken, address _inflationRewardToken) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -86,9 +89,22 @@ contract SymbioticStaking is _grantRole(DEFAULT_ADMIN_ROLE, _admin); + require(_stakingManager != address(0), "SymbioticStaking: stakingManager is zero"); stakingManager = _stakingManager; + + require(_rewardDistributor != address(0), "SymbioticStaking: rewardDistributor is zero"); rewardDistributor = _rewardDistributor; + + require(_inflationRewardManager != address(0), "SymbioticStaking: inflationRewardManager is zero"); + inflationRewardManager = _inflationRewardManager; + + require(_feeRewardToken != address(0), "SymbioticStaking: feeRewardToken is zero"); + feeRewardToken = _feeRewardToken; + + require(_inflationRewardToken != address(0), "SymbioticStaking: inflationRewardToken is zero"); + inflationRewardToken = _inflationRewardToken; } + /*===================================================== external ====================================================*/ @@ -165,35 +181,42 @@ contract SymbioticStaking is address _token = _selectLockToken(); require(getOperatorActiveStakeAmount(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); - // Store transmitter address to reward when job is closed - uint256 timestampIdx = confirmedTimestamps.length - 1; - - lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token], timestampIdx); + lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token]); operatorLockedAmounts[_operator][_token] += amountToLock[_token]; // TODO: emit event } - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) external onlyStakingManager { + function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount, uint256 _inflationRewardTimestampIdx) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; - uint256 timestampIdx = lock.timestampIdx; - uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[timestampIdx].transmitterComissionRate, 1e18); - - uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; + // currentEpoch => currentTimestampIdx + IInflationRewardManager(inflationRewardManager).updateEpochTimestampIdx(); // distribute fee reward - if(feeRewardRemaining > 0) { + if(_feeRewardAmount > 0) { + uint256 currentTimestampIdx = latestConfirmedTimestampIdx(); + uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[currentTimestampIdx].transmitterComissionRate, 1e18); + uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; + + // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation + IERC20(feeRewardToken).safeTransfer(confirmedTimestamps[currentTimestampIdx].transmitter, transmitterComission); + + // distribute the remaining fee reward _distributeFeeReward(lock.stakeToken, _operator, feeRewardRemaining); } // distribute inflation reward if(_inflationRewardAmount > 0) { - _distributeInflationReward(_operator, _inflationRewardAmount); - } + uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[_inflationRewardTimestampIdx].transmitterComissionRate, 1e18); + uint256 inflationRewardRemaining = _inflationRewardAmount - transmitterComission; - // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation - IERC20(feeRewardToken).safeTransfer(confirmedTimestamps[timestampIdx].transmitter, transmitterComission); + // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation + IERC20(inflationRewardToken).safeTransfer(confirmedTimestamps[_inflationRewardTimestampIdx].transmitter, transmitterComission); + + // distribute the remaining inflation reward + _distributeInflationReward(_operator, inflationRewardRemaining); + } // unlock the stake locked during job creation delete lockInfo[_jobId]; @@ -273,6 +296,7 @@ contract SymbioticStaking is Struct.ConfirmedTimestamp memory confirmedTimestamp = Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); confirmedTimestamps.push(confirmedTimestamp); + IInflationRewardManager(inflationRewardManager).updateEpochTimestampIdx(); // TODO: emit event } @@ -289,13 +313,16 @@ contract SymbioticStaking is /*================================================== external view ==================================================*/ + function latestConfirmedTimestamp() public view returns (uint256) { + return confirmedTimestamps[latestConfirmedTimestampIdx()].captureTimestamp; + } - function lastConfirmedTimestamp() public view returns (uint256) { - return confirmedTimestamps[confirmedTimestamps.length - 1].captureTimestamp; + function latestConfirmedTimestampIdx() public view returns (uint256) { + return confirmedTimestamps.length - 1; } function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakeAmounts[lastConfirmedTimestamp()][_operator][_token]; + return operatorStakeAmounts[latestConfirmedTimestamp()][_operator][_token]; } function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { @@ -305,7 +332,7 @@ contract SymbioticStaking is } function getStakeAmount(address _vault, address _stakeToken, address _operator) external view returns (uint256) { - return vaultStakeAmounts[lastConfirmedTimestamp()][_vault][_stakeToken][_operator]; + return vaultStakeAmounts[latestConfirmedTimestamp()][_vault][_stakeToken][_operator]; } function getStakeTokenList() external view returns(address[] memory) { @@ -324,7 +351,7 @@ contract SymbioticStaking is require(_numOfTxs > 0, "Invalid length"); // snapshot cannot be submitted before the cooldown period from the last confirmed timestamp (completed snapshot submission) - require(block.timestamp >= (lastConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); + require(block.timestamp >= (latestConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; @@ -352,6 +379,10 @@ contract SymbioticStaking is // TODO: (block.timestamp - _lastConfirmedTimestamp) * X } + function _currentTransmitter() internal view returns (address) { + return confirmedTimestamps[latestConfirmedTimestampIdx()].transmitter; + } + /*-------------------------------------- Job -------------------------------------*/ // TODO: weight based random selection diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 488f908..3126ec7 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -119,7 +119,6 @@ contract SymbioticStakingReward is } } - /* ------------------------- reward claim ------------------------- */ /// @notice vault can claim reward calling this function From f6f8231ee7459a3997f2112a6ef192abcd25f210 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 10 Oct 2024 18:18:35 +0900 Subject: [PATCH 146/158] fix timestampIdx logic --- .../interfaces/staking/IStakingManager.sol | 2 +- contracts/interfaces/staking/IStakingPool.sol | 2 +- .../l2_contracts/InflationRewardManger.sol | 4 +++- .../staking/l2_contracts/NativeStaking.sol | 2 +- .../staking/l2_contracts/StakingManager.sol | 4 ++-- .../staking/l2_contracts/SymbioticStaking.sol | 20 +++++++++++++------ 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index 3818a4f..b4b6443 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -11,5 +11,5 @@ interface IStakingManager { function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; - function distributeInflationReward(address operator, uint256 rewardAmount) external; + function distributeInflationReward(address operator, uint256 rewardAmount, uint256 timestampIdx) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index 60287e4..ca26f52 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -18,7 +18,7 @@ interface IStakingPool { function rewardDistributor() external view returns (address); - function distributeInflationReward(address _operator, uint256 _rewardAmount) external; // Staking Manager only + function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) external; // Staking Manager only function getStakeTokenList() external view returns (address[] memory); diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 131d17a..aea5070 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -127,6 +127,8 @@ contract InflationRewardManager is return (0, 0); } + timestampIdx = epochTimestampIdx[operatorLastEpoch]; + // when operator has done job in last epoch, distribute inflation reward // if 0, it means pendingInflationReward was updated and no job has been done if(operatorLastEpochJobCount > 0) { @@ -149,7 +151,7 @@ contract InflationRewardManager is // when job is completed, inflation reward with distributed by JobManager along with fee reward if(msg.sender != stakingManager && pendingInflationReward > 0) { // staking manager will distribute inflation reward based on each pool's share - IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward); + IStakingManager(stakingManager).distributeInflationReward(_operator, pendingInflationReward, timestampIdx); } lastJobCompletionEpochs[_operator] = currentEpoch; diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 4fce612..966fdc3 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -194,7 +194,7 @@ contract NativeStaking is // TODO: emit event } - function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { + function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 /* _timestampIdx */) external onlyStakingManager { if (_rewardAmount == 0) return; // _distributeInflationReward(_operator, _rewardAmount); diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index bb7f84d..05374c8 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -123,7 +123,7 @@ contract StakingManager is } } - function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyInflationRewardManager { + function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) external onlyInflationRewardManager { if(_rewardAmount == 0) return; uint256 len = stakingPoolSet.length(); @@ -132,7 +132,7 @@ contract StakingManager is (, uint256 poolRewardAmount) = _calcRewardAmount(pool, 0, _rewardAmount); - IStakingPool(pool).distributeInflationReward(_operator, poolRewardAmount); + IStakingPool(pool).distributeInflationReward(_operator, poolRewardAmount, _timestampIdx); } } diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 297ac88..f40eeb3 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -179,10 +179,11 @@ contract SymbioticStaking is function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { address _token = _selectLockToken(); - require(getOperatorActiveStakeAmount(_operator, _token) >= amountToLock[_token], "Insufficient stake amount"); + uint256 _amountToLock = amountToLock[_token]; + require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake amount"); - lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, amountToLock[_token]); - operatorLockedAmounts[_operator][_token] += amountToLock[_token]; + lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, _amountToLock); + operatorLockedAmounts[_operator][_token] += _amountToLock; // TODO: emit event } @@ -208,7 +209,7 @@ contract SymbioticStaking is // distribute inflation reward if(_inflationRewardAmount > 0) { - uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[_inflationRewardTimestampIdx].transmitterComissionRate, 1e18); + uint256 transmitterComission = Math.mulDiv(_inflationRewardAmount, confirmedTimestamps[_inflationRewardTimestampIdx].transmitterComissionRate, 1e18); uint256 inflationRewardRemaining = _inflationRewardAmount - transmitterComission; // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation @@ -242,12 +243,19 @@ contract SymbioticStaking is } } - function distributeInflationReward(address _operator, uint256 _rewardAmount) external onlyStakingManager { + /// @notice called when pending inflation reward is updated + function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) external onlyStakingManager { if(_rewardAmount == 0) return; + uint256 transmitterComission = Math.mulDiv(_rewardAmount, confirmedTimestamps[_timestampIdx].transmitterComissionRate, 1e18); + uint256 inflationRewardRemaining = _rewardAmount - transmitterComission; + + // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation + IERC20(inflationRewardToken).safeTransfer(confirmedTimestamps[_timestampIdx].transmitter, transmitterComission); + uint256 len = stakeTokenSet.length(); for(uint256 i = 0; i < len; i++) { - _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), _rewardAmount)); // TODO: gas optimization + _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), inflationRewardRemaining)); // TODO: gas optimization } } From 1376f23eb9ca3ff606f134757876cbdac9fc68e9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Thu, 10 Oct 2024 20:43:33 +0900 Subject: [PATCH 147/158] implement weight based token selection --- .../staking/l2_contracts/SymbioticStaking.sol | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index f40eeb3..fee82f8 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -49,10 +49,13 @@ contract SymbioticStaking is address public feeRewardToken; address public inflationRewardToken; + + uint256 public tokenSelectionWeightSum; /* Config */ - mapping(address token => uint256 amount) public amountToLock; + mapping(address stakeToken => uint256 amount) public amountToLock; + mapping(address stakeToken => uint256 weight) public tokenSelectionWeight; mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% @@ -178,7 +181,7 @@ contract SymbioticStaking is /*--------------------------- stake lock/unlock for job --------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectLockToken(); + address _token = _selectStakeToken(); uint256 _amountToLock = amountToLock[_token]; require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake amount"); @@ -393,16 +396,66 @@ contract SymbioticStaking is /*-------------------------------------- Job -------------------------------------*/ - // TODO: weight based random selection - function _selectLockToken() internal view returns(address) { - require(stakeTokenSet.length() > 0, "No supported token"); + function _selectStakeToken() internal view returns(address) { + require(tokenSelectionWeightSum > 0, "Total weight must be greater than zero"); + require(stakeTokenSet.length() > 0, "No tokens available"); + + address[] memory tokens = new address[](stakeTokenSet.length()); + uint256[] memory weights = new uint256[](stakeTokenSet.length()); + + uint256 weightSum = tokenSelectionWeightSum; + + uint256 idx = 0; + uint256 len = stakeTokenSet.length(); + for (uint256 i = 0; i < len; i++) { + address token = stakeTokenSet.at(i); + uint256 weight = tokenSelectionWeight[token]; + // ignore if weight is 0 + if (weight > 0) { + tokens[idx] = token; + weights[idx] = weight; + idx++; + } + } - uint256 idx; - if (stakeTokenSet.length() > 1) { - uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - idx = randomNumber % stakeTokenSet.length(); + // repeat until a valid token is selected + while (true) { + require(idx > 0, "No stakeToken available"); + + // random number in range [0, weightSum - 1] + uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1), msg.sender))) % weightSum; + + uint256 cumulativeWeight = 0; + address selectedToken; + + + uint256 i; + // select token based on weight + for (i = 0; i < idx; i++) { + cumulativeWeight += weights[i]; + if (random < cumulativeWeight) { + selectedToken = tokens[i]; + break; + } + } + + // check if the selected token has enough active stake amount + if (_getTotalActiveStakeAmount(selectedToken) >= amountToLock[selectedToken]) { + return selectedToken; + } + + weightSum -= weights[i]; + tokens[i] = tokens[idx - 1]; + weights[i] = weights[idx - 1]; + idx--; // 배열 크기를 줄임 } - return stakeTokenSet.at(idx); + + // this should be returned + return address(0); + } + + function _getTotalActiveStakeAmount(address _stakeToken) internal view returns(uint256) { + // TODO } function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { @@ -423,12 +476,18 @@ contract SymbioticStaking is // TODO: emit event } - function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_isSupported) { - require(stakeTokenSet.add(_token), "Token already exists"); - } else { - require(stakeTokenSet.remove(_token), "Token does not exist"); - } + function addStakeToken(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.add(_token), "Token already exists"); + + tokenSelectionWeightSum += _weight; + inflationRewardShare[_token] = _weight; + } + + function removeStakeToken(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.remove(_token), "Token does not exist"); + + tokenSelectionWeightSum -= inflationRewardShare[_token]; + delete inflationRewardShare[_token]; } function setSubmissionCooldown(uint256 _submissionCooldown) external onlyRole(DEFAULT_ADMIN_ROLE) { From 7424f806a0e1260348e46a1e1b9e8d3a6740994a Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 00:22:03 +0900 Subject: [PATCH 148/158] add gaps --- .gitmodules | 9 +++++++++ .../staking/l2_contracts/InflationRewardManger.sol | 6 ++++++ contracts/staking/l2_contracts/JobManager.sol | 13 ++++++++----- contracts/staking/l2_contracts/NativeStaking.sol | 6 ++++++ contracts/staking/l2_contracts/StakingManager.sol | 10 ++++++++-- contracts/staking/l2_contracts/SymbioticStaking.sol | 11 +++++++++-- .../staking/l2_contracts/SymbioticStakingReward.sol | 6 ++++++ 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/.gitmodules b/.gitmodules index 888d42d..3bad027 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index aea5070..23949e3 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -26,6 +26,9 @@ contract InflationRewardManager is { using SafeERC20 for IERC20; + // gaps in case we new vars in same file + uint256[500] private __gap_0; + /* config */ uint256 public startTime; @@ -39,6 +42,9 @@ contract InflationRewardManager is uint256 public inflationRewardEpochSize; uint256 public inflationRewardPerEpoch; + // gaps in case we new vars in same file + uint256[500] private __gap_1; + // last epoch when operator completed a job mapping(address operator => uint256 lastJobCompletionEpoch) lastJobCompletionEpochs; diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index fb9b369..e213b18 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -35,7 +35,8 @@ contract JobManager is // operator deducts comission from inflation reward mapping(address operator => uint256 rewardShare) public operatorRewardShares; // 1e18 == 100% - uint256 public startTime; // TODO: immutable? + // gaps in case we new vars in same file + uint256[500] private __gap_0; address public stakingManager; address public feeToken; @@ -43,9 +44,12 @@ contract JobManager is uint256 public jobDuration; + // gaps in case we new vars in same file + uint256[500] private __gap_1; + /*======================================== Init ========================================*/ - function initialize(uint256 _startTime, address _admin, address _stakingManager, address _feeToken, address _inflationRewardManager, uint256 _jobDuration) + function initialize(address _admin, address _stakingManager, address _feeToken, address _inflationRewardManager, uint256 _jobDuration) public initializer { @@ -56,9 +60,6 @@ contract JobManager is _grantRole(DEFAULT_ADMIN_ROLE, _admin); - require(_startTime >= block.timestamp, "JobManager: Invalid Start Time"); - startTime = _startTime; - require(_stakingManager != address(0), "JobManager: Invalid StakingManager"); stakingManager = _stakingManager; @@ -183,4 +184,6 @@ contract JobManager is } function _authorizeUpgrade(address /*account*/ ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + } diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 966fdc3..840a85c 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -30,6 +30,9 @@ contract NativeStaking is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; + // gaps in case we new vars in same file + uint256[500] private __gap_0; + EnumerableSet.AddressSet private stakeTokenSet; address public rewardDistributor; @@ -37,6 +40,9 @@ contract NativeStaking is address public feeRewardToken; address public inflationRewardToken; + // gaps in case we new vars in same file + uint256[500] private __gap_1; + /* Config */ uint256 public withdrawalDuration; mapping(address stakeToken => uint256 lockAmount) public amountToLock; // amount of token to lock for each job creation diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 05374c8..1dbd8f0 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -30,6 +30,12 @@ contract StakingManager is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; + mapping(address pool => Struct.PoolConfig config) private poolConfig; + mapping(address pool => uint256 weight) private stakingPoolWeight; + + // gaps in case we new vars in same file + uint256[500] private __gap_0; + EnumerableSet.AddressSet private stakingPoolSet; address public jobManager; @@ -38,8 +44,8 @@ contract StakingManager is address public feeToken; address public inflationRewardToken; - mapping(address pool => Struct.PoolConfig config) private poolConfig; - mapping(address pool => uint256 weight) private stakingPoolWeight; + // gaps in case we new vars in same file + uint256[500] private __gap_1; modifier onlyJobManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index fee82f8..0f04124 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -30,8 +30,8 @@ contract SymbioticStaking is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - uint256 submissionCooldown; // 18 decimal (in seconds) - uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) + // gaps in case we new vars in same file + uint256[500] private __gap_0; /* Job Status */ bytes32 public constant STAKE_SNAPSHOT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; @@ -39,6 +39,10 @@ contract SymbioticStaking is bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000011; bytes32 public constant STAKE_SNAPSHOT_TYPE = keccak256("STAKE_SNAPSHOT"); + + uint256 submissionCooldown; // 18 decimal (in seconds) + uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) + bytes32 public constant SLASH_RESULT_TYPE = keccak256("SLASH_RESULT"); EnumerableSet.AddressSet stakeTokenSet; @@ -51,6 +55,9 @@ contract SymbioticStaking is address public inflationRewardToken; uint256 public tokenSelectionWeightSum; + + // gaps in case we new vars in same file + uint256[500] private __gap_1; /* Config */ diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 3126ec7..05551af 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -31,6 +31,10 @@ contract SymbioticStakingReward is using EnumerableSet for EnumerableSet.AddressSet; using Math for uint256; + // gaps in case we new vars in same file + uint256[500] private __gap_0; + + address public jobManager; address public symbioticStaking; address public inflationRewardManager; @@ -38,6 +42,8 @@ contract SymbioticStakingReward is address public feeRewardToken; address public inflationRewardToken; + // gaps in case we new vars in same file + uint256[500] private __gap_1; // rewardTokens amount per stakeToken mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) From 57ac65da98ead24822f173c52c2a73784d6b8bac Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 00:22:25 +0900 Subject: [PATCH 149/158] foundry test setup --- test/foundry/TestSetup.t.sol | 181 +++++++++++++++++++++++++++++++++++ test/foundry/mocks/POND.sol | 14 +++ test/foundry/mocks/USDC.sol | 14 +++ 3 files changed, 209 insertions(+) create mode 100644 test/foundry/TestSetup.t.sol create mode 100644 test/foundry/mocks/POND.sol create mode 100644 test/foundry/mocks/USDC.sol diff --git a/test/foundry/TestSetup.t.sol b/test/foundry/TestSetup.t.sol new file mode 100644 index 0000000..3947458 --- /dev/null +++ b/test/foundry/TestSetup.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {Test, console} from "forge-std/Test.sol"; + +import {USDC} from "./mocks/USDC.sol"; +import {POND} from "./mocks/POND.sol"; + +import {JobManager} from "../../contracts/staking/l2_contracts/JobManager.sol"; +import {StakingManager} from "../../contracts/staking/l2_contracts/StakingManager.sol"; +import {NativeStaking} from "../../contracts/staking/l2_contracts/NativeStaking.sol"; +import {SymbioticStaking} from "../../contracts/staking/l2_contracts/SymbioticStaking.sol"; +import {SymbioticStakingReward} from "../../contracts/staking/l2_contracts/SymbioticStakingReward.sol"; +import {InflationRewardManager} from "../../contracts/staking/l2_contracts/InflationRewardManger.sol"; + +import {IJobManager} from "../../contracts/interfaces/staking/IJobManager.sol"; +import {IStakingManager} from "../../contracts/interfaces/staking/IStakingManager.sol"; +import {IInflationRewardManager} from "../../contracts/interfaces/staking/IInflationRewardManager.sol"; +import {INativeStaking} from "../../contracts/interfaces/staking/INativeStaking.sol"; +import {ISymbioticStaking} from "../../contracts/interfaces/staking/ISymbioticStaking.sol"; +import {ISymbioticStakingReward} from "../../contracts/interfaces/staking/ISymbioticStakingReward.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract TestSetup is Test { + uint256 constant FUND_FOR_GAS = 10 * 1e18; // 10 ether + + address public jobManager; + address public stakingManager; + address public inflationRewardManager; + address public feeToken; + address public inflationRewardToken; + + address public nativeStaking; + address public symbioticStaking; + address public symbioticStakingReward; + + address public deployer; + address public admin; + + address public operatorA; + address public operatorB; + address public operatorC; + + address public userA; + address public userB; + address public userC; + + function _setupAddr() internal { + /* set address */ + deployer = makeAddr("deployer"); + admin = makeAddr("admin"); + + operatorA = makeAddr("operatorA"); + operatorB = makeAddr("operatorB"); + operatorC = makeAddr("operatorC"); + + userA = makeAddr("userA"); + userB = makeAddr("userB"); + userC = makeAddr("userC"); + + /* fund gas */ + vm.deal(deployer, FUND_FOR_GAS); + vm.deal(admin, FUND_FOR_GAS); + + vm.deal(operatorA, FUND_FOR_GAS); + vm.deal(operatorB, FUND_FOR_GAS); + vm.deal(operatorC, FUND_FOR_GAS); + + vm.deal(userA, FUND_FOR_GAS); + vm.deal(userB, FUND_FOR_GAS); + vm.deal(userC, FUND_FOR_GAS); + + /* label */ + vm.label(deployer, "deployer"); + vm.label(admin, "admin"); + + vm.label(operatorA, "operatorA"); + vm.label(operatorB, "operatorB"); + vm.label(operatorC, "operatorC"); + + vm.label(userA, "userA"); + vm.label(userB, "userB"); + vm.label(userC, "userC"); + } + + function _deployContracts() internal { + vm.startPrank(deployer); + + // FeeToken + feeToken = address(new USDC(admin)); + + // InflationRewardToken + inflationRewardToken = address(new POND(admin)); + + address jobManagerImpl = address(new JobManager()); + address stakingManagerImpl = address(new StakingManager()); + address nativeStakingImpl = address(new NativeStaking()); + address symbioticStakingImpl = address(new SymbioticStaking()); + address symbioticStakingRewardImpl = address(new SymbioticStakingReward()); + address inflationRewardManagerImpl = address(new InflationRewardManager()); + + jobManager = address(new ERC1967Proxy(jobManagerImpl, "")); + stakingManager = address(new ERC1967Proxy(stakingManagerImpl, "")); + nativeStaking = address(new ERC1967Proxy(nativeStakingImpl, "")); + symbioticStaking = address(new ERC1967Proxy(symbioticStakingImpl, "")); + symbioticStakingReward = address(new ERC1967Proxy(symbioticStakingRewardImpl, "")); + inflationRewardManager = address(new ERC1967Proxy(inflationRewardManagerImpl, "")); + vm.stopPrank(); + + vm.label(address(jobManager), "JobManager"); + vm.label(address(stakingManager), "StakingManager"); + vm.label(address(nativeStaking), "NativeStaking"); + vm.label(address(symbioticStaking), "SymbioticStaking"); + vm.label(address(symbioticStakingReward), "SymbioticStakingReward"); + vm.label(address(inflationRewardManager), "InflationRewardManager"); + } + + function _initializeContracts() internal { + vm.startPrank(admin); + + // JobManager + JobManager(address(jobManager)).initialize( + admin, address(stakingManager), address(feeToken), address(inflationRewardManager), 1 hours + ); + + // StakingManager + StakingManager(address(stakingManager)).initialize( + admin, + address(jobManager), + address(inflationRewardManager), + address(feeToken), + address(inflationRewardToken) + ); + + // NativeStaking + NativeStaking(address(nativeStaking)).initialize( + admin, + address(stakingManager), + address(0), // rewardDistributor (not set) + 2 days, // withdrawalDuration + address(feeToken), + address(inflationRewardToken) + ); + + // SymbioticStaking + SymbioticStaking(address(symbioticStaking)).initialize( + admin, + address(stakingManager), + address(symbioticStakingReward), + address(inflationRewardManager), + address(feeToken), + address(inflationRewardToken) + ); + + // SymbioticStakingReward + SymbioticStakingReward(address(symbioticStakingReward)).initialize( + admin, + address(inflationRewardManager), + address(jobManager), + address(symbioticStaking), + address(feeToken), + address(inflationRewardToken) + ); + + // InflationRewardManager + InflationRewardManager(address(inflationRewardManager)).initialize( + admin, + block.timestamp, + address(jobManager), + address(stakingManager), + address(inflationRewardToken), + 30 minutes, // inflationRewardEpochSize + 1000 ether // inflationRewardPerEpoch + ); + + vm.stopPrank(); + } +} diff --git a/test/foundry/mocks/POND.sol b/test/foundry/mocks/POND.sol new file mode 100644 index 0000000..da3b1db --- /dev/null +++ b/test/foundry/mocks/POND.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract POND is ERC20 { + + uint256 constant INITIAL_SUPPLY = 100_000_000 ether; + + constructor(address admin) ERC20("POND", "POND") { + _mint(admin, INITIAL_SUPPLY); + } +} diff --git a/test/foundry/mocks/USDC.sol b/test/foundry/mocks/USDC.sol new file mode 100644 index 0000000..fd32ee1 --- /dev/null +++ b/test/foundry/mocks/USDC.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract USDC is ERC20 { + + uint256 constant INITIAL_SUPPLY = 100_000_000 ether; + + constructor(address admin) ERC20("USDC", "USDC") { + _mint(admin, INITIAL_SUPPLY); + } +} From a245830c4b8b7c2e65cbe286fbc079c94e263449 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 01:09:45 +0900 Subject: [PATCH 150/158] consider the case when inflation reward is not enough --- .../staking/l2_contracts/InflationRewardManger.sol | 7 ++++++- test/foundry/TestSetup.t.sol | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 23949e3..9b4dd1f 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -139,11 +139,16 @@ contract InflationRewardManager is // if 0, it means pendingInflationReward was updated and no job has been done if(operatorLastEpochJobCount > 0) { uint256 lastEpochTotalJobCount = totalJobCountsPerEpoch[operatorLastEpoch]; - + pendingInflationReward = Math.mulDiv( inflationRewardPerEpoch, operatorLastEpochJobCount, lastEpochTotalJobCount ); + // TODO: check logic + if(pendingInflationReward > IERC20(inflationRewardToken).balanceOf(address(this))) { + pendingInflationReward = IERC20(inflationRewardToken).balanceOf(address(this)); + } + // operator deducts comission from inflation reward uint256 operatorComission = Math.mulDiv( pendingInflationReward, IJobManager(jobManager).operatorRewardShares(_operator), 1e18 diff --git a/test/foundry/TestSetup.t.sol b/test/foundry/TestSetup.t.sol index 3947458..e353c14 100644 --- a/test/foundry/TestSetup.t.sol +++ b/test/foundry/TestSetup.t.sol @@ -125,6 +125,7 @@ contract TestSetup is Test { JobManager(address(jobManager)).initialize( admin, address(stakingManager), address(feeToken), address(inflationRewardManager), 1 hours ); + assertEq(JobManager(jobManager).hasRole(JobManager(jobManager).DEFAULT_ADMIN_ROLE(), admin), true); // StakingManager StakingManager(address(stakingManager)).initialize( @@ -134,6 +135,7 @@ contract TestSetup is Test { address(feeToken), address(inflationRewardToken) ); + assertEq(StakingManager(stakingManager).hasRole(StakingManager(stakingManager).DEFAULT_ADMIN_ROLE(), admin), true); // NativeStaking NativeStaking(address(nativeStaking)).initialize( @@ -144,7 +146,8 @@ contract TestSetup is Test { address(feeToken), address(inflationRewardToken) ); - + assertEq(NativeStaking(nativeStaking).hasRole(NativeStaking(nativeStaking).DEFAULT_ADMIN_ROLE(), admin), true); + // SymbioticStaking SymbioticStaking(address(symbioticStaking)).initialize( admin, @@ -154,7 +157,7 @@ contract TestSetup is Test { address(feeToken), address(inflationRewardToken) ); - + assertEq(SymbioticStaking(symbioticStaking).hasRole(SymbioticStaking(symbioticStaking).DEFAULT_ADMIN_ROLE(), admin), true); // SymbioticStakingReward SymbioticStakingReward(address(symbioticStakingReward)).initialize( admin, @@ -164,6 +167,7 @@ contract TestSetup is Test { address(feeToken), address(inflationRewardToken) ); + assertEq(SymbioticStakingReward(symbioticStakingReward).hasRole(SymbioticStakingReward(symbioticStakingReward).DEFAULT_ADMIN_ROLE(), admin), true); // InflationRewardManager InflationRewardManager(address(inflationRewardManager)).initialize( @@ -175,7 +179,7 @@ contract TestSetup is Test { 30 minutes, // inflationRewardEpochSize 1000 ether // inflationRewardPerEpoch ); - + assertEq(InflationRewardManager(inflationRewardManager).hasRole(InflationRewardManager(inflationRewardManager).DEFAULT_ADMIN_ROLE(), admin), true); vm.stopPrank(); } } From 9a6019f197f4c7dacf1946c4ea934304d8c21dd7 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 11:27:49 +0900 Subject: [PATCH 151/158] minor fix --- contracts/lib/staking/Struct.sol | 2 +- contracts/staking/l2_contracts/JobManager.sol | 4 ++++ contracts/staking/l2_contracts/StakingManager.sol | 10 +++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/lib/staking/Struct.sol b/contracts/lib/staking/Struct.sol index 1519d95..c9142e8 100644 --- a/contracts/lib/staking/Struct.sol +++ b/contracts/lib/staking/Struct.sol @@ -14,7 +14,7 @@ library Struct { /*========================= Staking Manager ===========================*/ struct PoolConfig { - uint256 weight; + uint256 share; bool enabled; } diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index e213b18..ff1bbfe 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -171,6 +171,10 @@ contract JobManager is jobDuration = _jobDuration; } + function setOperatorRewardShare(address _operator, uint256 _rewardShare) external onlyRole(DEFAULT_ADMIN_ROLE) { + operatorRewardShares[_operator] = _rewardShare; + } + /*======================================== Overrides ========================================*/ function supportsInterface(bytes4 interfaceId) diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index 1dbd8f0..b5250db 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -143,10 +143,10 @@ contract StakingManager is } function _calcRewardAmount(address _pool, uint256 _feeRewardAmount, uint256 _inflationRewardAmount) internal view returns (uint256, uint256) { - uint256 poolWeight = poolConfig[_pool].weight; + uint256 poolShare = poolConfig[_pool].share; - uint256 poolFeeRewardAmount = _feeRewardAmount > 0 ? Math.mulDiv(_feeRewardAmount, poolWeight, 1e18) : 0; - uint256 poolInflationRewardAmount = _inflationRewardAmount > 0 ? Math.mulDiv(_inflationRewardAmount, poolWeight, 1e18) : 0; + uint256 poolFeeRewardAmount = _feeRewardAmount > 0 ? Math.mulDiv(_feeRewardAmount, poolShare, 1e18) : 0; + uint256 poolInflationRewardAmount = _inflationRewardAmount > 0 ? Math.mulDiv(_inflationRewardAmount, poolShare, 1e18) : 0; return (poolFeeRewardAmount, poolInflationRewardAmount); } @@ -178,7 +178,7 @@ contract StakingManager is } // when job is closed, the reward will be distributed based on the share - function setShare(address[] calldata _pools, uint256[] calldata _shares) + function setPoolShare(address[] calldata _pools, uint256[] calldata _shares) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -186,7 +186,7 @@ contract StakingManager is uint256 sum = 0; for (uint256 i = 0; i < _shares.length; i++) { - poolConfig[_pools[i]].weight = _shares[i]; + poolConfig[_pools[i]].share = _shares[i]; sum += _shares[i]; } From 30e11a2f7a8577e50206c1d873c2d9a6114f2930 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 14:11:54 +0900 Subject: [PATCH 152/158] test setup --- .../interfaces/staking/INativeStaking.sol | 6 +- .../interfaces/staking/IStakingManager.sol | 2 + test/foundry/TestSetup.t.sol | 166 ++++++++++++++++-- test/foundry/mocks/WETH.sol | 14 ++ 4 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 test/foundry/mocks/WETH.sol diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 166989e..9f260ed 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -5,11 +5,9 @@ import {IStakingPool} from "../staking/IStakingPool.sol"; interface INativeStaking is IStakingPool { + function stake(address operator, address stakeToken, uint256 amount) external; + // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); event StakeWithdrawn(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); - - // function stakeOf(address _operator, address _token) external view returns (uint256); - - // function stakesOf(address _operator) external view returns (address[] memory _tokens, uint256[] memory _amounts); } \ No newline at end of file diff --git a/contracts/interfaces/staking/IStakingManager.sol b/contracts/interfaces/staking/IStakingManager.sol index b4b6443..3090b38 100644 --- a/contracts/interfaces/staking/IStakingManager.sol +++ b/contracts/interfaces/staking/IStakingManager.sol @@ -12,4 +12,6 @@ interface IStakingManager { function onSlashResult(Struct.JobSlashed[] calldata slashedJobs) external; function distributeInflationReward(address operator, uint256 rewardAmount, uint256 timestampIdx) external; + + function getPoolConfig(address pool) external view returns (Struct.PoolConfig memory); } \ No newline at end of file diff --git a/test/foundry/TestSetup.t.sol b/test/foundry/TestSetup.t.sol index e353c14..a9e4a01 100644 --- a/test/foundry/TestSetup.t.sol +++ b/test/foundry/TestSetup.t.sol @@ -4,9 +4,12 @@ pragma solidity ^0.8.26; import {Test, console} from "forge-std/Test.sol"; +/* mocks */ import {USDC} from "./mocks/USDC.sol"; import {POND} from "./mocks/POND.sol"; +import {WETH} from "./mocks/WETH.sol"; +/* contracts */ import {JobManager} from "../../contracts/staking/l2_contracts/JobManager.sol"; import {StakingManager} from "../../contracts/staking/l2_contracts/StakingManager.sol"; import {NativeStaking} from "../../contracts/staking/l2_contracts/NativeStaking.sol"; @@ -14,6 +17,9 @@ import {SymbioticStaking} from "../../contracts/staking/l2_contracts/SymbioticSt import {SymbioticStakingReward} from "../../contracts/staking/l2_contracts/SymbioticStakingReward.sol"; import {InflationRewardManager} from "../../contracts/staking/l2_contracts/InflationRewardManger.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/* interfaces */ import {IJobManager} from "../../contracts/interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../contracts/interfaces/staking/IStakingManager.sol"; import {IInflationRewardManager} from "../../contracts/interfaces/staking/IInflationRewardManager.sol"; @@ -22,56 +28,98 @@ import {ISymbioticStaking} from "../../contracts/interfaces/staking/ISymbioticSt import {ISymbioticStakingReward} from "../../contracts/interfaces/staking/ISymbioticStakingReward.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +/* libraries */ +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract TestSetup is Test { - uint256 constant FUND_FOR_GAS = 10 * 1e18; // 10 ether + uint256 constant TWENTY_PERCENT = 20; + uint256 constant THIRTY_PERCENT = 30; + uint256 constant FORTY_PERCENT = 40; + uint256 constant FIFTY_PERCENT = 50; + uint256 constant SIXTY_PERCENT = 60; + uint256 constant HUNDRED_PERCENT = 100; + + uint256 constant FUND_FOR_GAS = 10 ether; // 10 ether + uint256 constant FUND_FOR_FEE = 10_000 ether; // 10,000 USDC + uint256 constant FUND_FOR_INFLATION_REWARD = 100_000 ether; // 100,000 POND + + uint256 constant INFLATION_REWARD_EPOCH_SIZE = 30 minutes; // 30 minutes + uint256 constant INFLATION_REWARD_PER_EPOCH = 1000 ether; // 1,000 POND + + /* contracts */ address public jobManager; - address public stakingManager; address public inflationRewardManager; - address public feeToken; - address public inflationRewardToken; + address public stakingManager; address public nativeStaking; address public symbioticStaking; address public symbioticStakingReward; + /* reward tokens */ + address public feeToken; + address public inflationRewardToken; + + /* stake tokens */ + address public usdc; + address public pond; + address public weth; + + /* admin */ address public deployer; address public admin; + address public vault; // holds inflation reward tokens + /* operators */ address public operatorA; address public operatorB; address public operatorC; - address public userA; - address public userB; - address public userC; + /* stakers */ + address public stakerA; + address public stakerB; + address public stakerC; + + /* job requesters */ + address public jobRequesterA; + address public jobRequesterB; + address public jobRequesterC; + function _setupAddr() internal { /* set address */ deployer = makeAddr("deployer"); admin = makeAddr("admin"); + vault = makeAddr("vault"); + + stakerA = makeAddr("stakerA"); + stakerB = makeAddr("stakerB"); + stakerC = makeAddr("stakerC"); operatorA = makeAddr("operatorA"); operatorB = makeAddr("operatorB"); operatorC = makeAddr("operatorC"); - userA = makeAddr("userA"); - userB = makeAddr("userB"); - userC = makeAddr("userC"); + jobRequesterA = makeAddr("jobRequesterA"); + jobRequesterB = makeAddr("jobRequesterB"); + jobRequesterC = makeAddr("jobRequesterC"); /* fund gas */ vm.deal(deployer, FUND_FOR_GAS); vm.deal(admin, FUND_FOR_GAS); + vm.deal(vault, FUND_FOR_GAS); vm.deal(operatorA, FUND_FOR_GAS); vm.deal(operatorB, FUND_FOR_GAS); vm.deal(operatorC, FUND_FOR_GAS); - vm.deal(userA, FUND_FOR_GAS); - vm.deal(userB, FUND_FOR_GAS); - vm.deal(userC, FUND_FOR_GAS); + vm.deal(stakerA, FUND_FOR_GAS); + vm.deal(stakerB, FUND_FOR_GAS); + vm.deal(stakerC, FUND_FOR_GAS); + + vm.deal(jobRequesterA, FUND_FOR_GAS); + vm.deal(jobRequesterB, FUND_FOR_GAS); + vm.deal(jobRequesterC, FUND_FOR_GAS); /* label */ vm.label(deployer, "deployer"); @@ -81,9 +129,20 @@ contract TestSetup is Test { vm.label(operatorB, "operatorB"); vm.label(operatorC, "operatorC"); - vm.label(userA, "userA"); - vm.label(userB, "userB"); - vm.label(userC, "userC"); + vm.label(stakerA, "stakerA"); + vm.label(stakerB, "stakerB"); + vm.label(stakerC, "stakerC"); + + vm.label(jobRequesterA, "jobRequesterA"); + vm.label(jobRequesterB, "jobRequesterB"); + vm.label(jobRequesterC, "jobRequesterC"); + } + + /*======================================== internal ========================================*/ + + function _setupContracts() internal { + _deployContracts(); + _initializeContracts(); } function _deployContracts() internal { @@ -95,6 +154,11 @@ contract TestSetup is Test { // InflationRewardToken inflationRewardToken = address(new POND(admin)); + // stakeToken + weth = address(new WETH(admin)); + pond = inflationRewardToken; + + // contract implementations address jobManagerImpl = address(new JobManager()); address stakingManagerImpl = address(new StakingManager()); address nativeStakingImpl = address(new NativeStaking()); @@ -102,6 +166,7 @@ contract TestSetup is Test { address symbioticStakingRewardImpl = address(new SymbioticStakingReward()); address inflationRewardManagerImpl = address(new InflationRewardManager()); + // deploy proxies jobManager = address(new ERC1967Proxy(jobManagerImpl, "")); stakingManager = address(new ERC1967Proxy(stakingManagerImpl, "")); nativeStaking = address(new ERC1967Proxy(nativeStakingImpl, "")); @@ -110,6 +175,7 @@ contract TestSetup is Test { inflationRewardManager = address(new ERC1967Proxy(inflationRewardManagerImpl, "")); vm.stopPrank(); + /* label */ vm.label(address(jobManager), "JobManager"); vm.label(address(stakingManager), "StakingManager"); vm.label(address(nativeStaking), "NativeStaking"); @@ -176,10 +242,72 @@ contract TestSetup is Test { address(jobManager), address(stakingManager), address(inflationRewardToken), - 30 minutes, // inflationRewardEpochSize - 1000 ether // inflationRewardPerEpoch + INFLATION_REWARD_EPOCH_SIZE, // inflationRewardEpochSize + INFLATION_REWARD_PER_EPOCH // inflationRewardPerEpoch ); assertEq(InflationRewardManager(inflationRewardManager).hasRole(InflationRewardManager(inflationRewardManager).DEFAULT_ADMIN_ROLE(), admin), true); vm.stopPrank(); } + + function _setJobManagerConfig() internal { + vm.startPrank(admin); + // operatorA: 30% of the reward as comission + JobManager(jobManager).setOperatorRewardShare(operatorA, _calcShareAmount(THIRTY_PERCENT)); + // operatorB: 50% of the reward as comission + JobManager(jobManager).setOperatorRewardShare(operatorB, _calcShareAmount(FIFTY_PERCENT)); + vm.stopPrank(); + } + + function _setStakingManagerConfig() internal { + address[] memory pools = new address[](2); + pools[0] = nativeStaking; + pools[1] = symbioticStaking; + + uint256[] memory shares = new uint256[](2); + shares[0] = 0; + shares[1] = _calcShareAmount(HUNDRED_PERCENT); + + vm.startPrank(admin); + StakingManager(stakingManager).addStakingPool(nativeStaking); + StakingManager(stakingManager).addStakingPool(symbioticStaking); + + StakingManager(stakingManager).setPoolRewardShare(pools, shares); + + StakingManager(stakingManager).setEnabledPool(nativeStaking, true); + StakingManager(stakingManager).setEnabledPool(symbioticStaking, true); + vm.stopPrank(); + + assertEq(IStakingManager(stakingManager).getPoolConfig(nativeStaking).share, 0); + assertEq(IStakingManager(stakingManager).getPoolConfig(symbioticStaking).share, _calcShareAmount(HUNDRED_PERCENT)); + } + + function _setNativeStakingConfig() internal { + vm.startPrank(admin); + NativeStaking(nativeStaking).setStakeToken(pond, true); + vm.stopPrank(); + } + + function _setSymbioticStakingConfig() internal { + vm.startPrank(admin); + + /* stake tokens and weights */ + SymbioticStaking(symbioticStaking).addStakeToken(pond, _calcShareAmount(SIXTY_PERCENT)); + SymbioticStaking(symbioticStaking).addStakeToken(weth, _calcShareAmount(FORTY_PERCENT)); + + /* base transmitter comission rate and submission cooldown */ + SymbioticStaking(symbioticStaking).setBaseTransmitterComissionRate(_calcShareAmount(TWENTY_PERCENT)); + SymbioticStaking(symbioticStaking).setSubmissionCooldown(12 hours); + vm.stopPrank(); + + assertEq(SymbioticStaking(symbioticStaking).baseTransmitterComissionRate(), _calcShareAmount(TWENTY_PERCENT)); + assertEq(SymbioticStaking(symbioticStaking).submissionCooldown(), 12 hours); + } + + + /*===================================== internal pure ======================================*/ + + /// @notice convert 100% -> 1e18 (i.e. 50 -> 50e17) + function _calcShareAmount(uint256 _shareIntPercentage) internal pure returns (uint256) { + return Math.mulDiv(_shareIntPercentage, 1e18, 100); + } } diff --git a/test/foundry/mocks/WETH.sol b/test/foundry/mocks/WETH.sol new file mode 100644 index 0000000..c3d70c5 --- /dev/null +++ b/test/foundry/mocks/WETH.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.26; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WETH is ERC20 { + + uint256 constant INITIAL_SUPPLY = 100_000_000 ether; + + constructor(address admin) ERC20("WETH", "WETH") { + _mint(admin, INITIAL_SUPPLY); + } +} From 22ddb9d312b5c057eab2312c0325691758fbe249 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 14:38:42 +0900 Subject: [PATCH 153/158] fix token selection algorithm in NativeStaking --- .../staking/l2_contracts/NativeStaking.sol | 96 +++++++++++++++---- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 840a85c..0dfa2a5 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -41,23 +41,26 @@ contract NativeStaking is address public inflationRewardToken; // gaps in case we new vars in same file - uint256[500] private __gap_1; /* Config */ uint256 public withdrawalDuration; + uint256 public tokenSelectionWeightSum; + mapping(address stakeToken => uint256 lockAmount) public amountToLock; // amount of token to lock for each job creation + mapping(address stakeToken => uint256 weight) public tokenSelectionWeight; mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% /* Stake */ - // total staked amounts for each operator - mapping(address operator => mapping(address stakeToken => uint256 stakeAmounts)) public operatorstakeAmounts; // staked amount for each account mapping(address account => mapping(address operator => mapping(address stakeToken => uint256 amount))) public stakeAmounts; + // total staked amounts for each operator + mapping(address operator => mapping(address stakeToken => uint256 stakeAmounts)) public operatorstakeAmounts; mapping(address account => mapping(address operator => Struct.WithdrawalRequest[] withdrawalRequest)) public withdrawalRequests; + uint256[500] private __gap_1; /* Locked Stakes */ mapping(uint256 jobId => Struct.NativeStakingLock lock) public lockInfo; mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; @@ -146,7 +149,7 @@ contract NativeStaking is /*-------------------------------- Satking Manager -------------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectTokenToLock(); + address _token = _selectStakeToken(_operator); uint256 _amountToLock = amountToLock[_token]; require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); @@ -280,15 +283,62 @@ contract NativeStaking is return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } - function _selectTokenToLock() internal view returns (address) { - require(stakeTokenSet.length() > 0, "No supported token"); + function _selectStakeToken(address _operator) internal view returns(address) { + require(tokenSelectionWeightSum > 0, "Total weight must be greater than zero"); + require(stakeTokenSet.length() > 0, "No tokens available"); + + address[] memory tokens = new address[](stakeTokenSet.length()); + uint256[] memory weights = new uint256[](stakeTokenSet.length()); + + uint256 weightSum = tokenSelectionWeightSum; - uint256 idx; - if (stakeTokenSet.length() > 1) { - uint256 randomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1)))); - idx = randomNumber % stakeTokenSet.length(); + uint256 idx = 0; + uint256 len = stakeTokenSet.length(); + for (uint256 i = 0; i < len; i++) { + address token = stakeTokenSet.at(i); + uint256 weight = tokenSelectionWeight[token]; + // ignore if weight is 0 + if (weight > 0) { + tokens[idx] = token; + weights[idx] = weight; + idx++; + } + } + + // repeat until a valid token is selected + while (true) { + require(idx > 0, "No stakeToken available"); + + // random number in range [0, weightSum - 1] + uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1), msg.sender))) % weightSum; + + uint256 cumulativeWeight = 0; + address selectedToken; + + + uint256 i; + // select token based on weight + for (i = 0; i < idx; i++) { + cumulativeWeight += weights[i]; + if (random < cumulativeWeight) { + selectedToken = tokens[i]; + break; + } + } + + // check if the selected token has enough active stake amount + if (getOperatorActiveStakeAmount(_operator, selectedToken) >= amountToLock[selectedToken]) { + return selectedToken; + } + + weightSum -= weights[i]; + tokens[i] = tokens[idx - 1]; + weights[i] = weights[idx - 1]; + idx--; // 배열 크기를 줄임 } - return stakeTokenSet.at(idx); + + // this should be returned + return address(0); } // function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { @@ -301,14 +351,24 @@ contract NativeStaking is /*====================================================== admin ======================================================*/ - function setStakeToken(address _token, bool _isSupported) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_isSupported) { - stakeTokenSet.add(_token); - } else { - stakeTokenSet.remove(_token); - } + function addStakeToken(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.add(_token), "Token already exists"); + + tokenSelectionWeightSum += _weight; + inflationRewardShare[_token] = _weight; + } - // TODO: emit event + function removeStakeToken(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.remove(_token), "Token does not exist"); + + tokenSelectionWeightSum -= inflationRewardShare[_token]; + delete inflationRewardShare[_token]; + } + + function setStakeTokenWeight(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + tokenSelectionWeightSum -= tokenSelectionWeight[_token]; + tokenSelectionWeight[_token] = _weight; + tokenSelectionWeightSum += _weight; } function setNativeStakingReward(address _nativeStakingReward) external onlyRole(DEFAULT_ADMIN_ROLE) { From a4b58a24f174fbeca3b330d5e9c5920424abd207 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 14:39:11 +0900 Subject: [PATCH 154/158] minor fix --- .../l2_contracts/InflationRewardManger.sol | 3 --- .../staking/l2_contracts/StakingManager.sol | 22 ++++++++++++---- .../staking/l2_contracts/SymbioticStaking.sol | 25 +++++++++++++------ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 9b4dd1f..818d936 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -48,9 +48,6 @@ contract InflationRewardManager is // last epoch when operator completed a job mapping(address operator => uint256 lastJobCompletionEpoch) lastJobCompletionEpochs; - // TODO: temporary - mapping(address operator => uint256 comissionRate) operatorInflationRewardComissionRate; // 1e18 == 100% - // count of jobs done by operator in an epoch mapping(uint256 epoch => mapping(address operator => uint256 count)) operatorJobCountsPerEpoch; // total count of jobs done in an epoch diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index b5250db..cad3588 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -30,11 +30,10 @@ contract StakingManager is using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - mapping(address pool => Struct.PoolConfig config) private poolConfig; - mapping(address pool => uint256 weight) private stakingPoolWeight; - // gaps in case we new vars in same file uint256[500] private __gap_0; + + mapping(address pool => Struct.PoolConfig config) private poolConfig; EnumerableSet.AddressSet private stakingPoolSet; @@ -102,6 +101,8 @@ contract StakingManager is for (uint256 i = 0; i < len; i++) { address pool = stakingPoolSet.at(i); + if(!isEnabledPool(pool)) continue; + (uint256 poolFeeRewardAmount, uint256 poolInflationRewardAmount) = _calcRewardAmount(pool, _feeRewardAmount, pendingInflationReward); IStakingPool(pool).onJobCompletion(_jobId, _operator, poolFeeRewardAmount, poolInflationRewardAmount, timestampIdx); @@ -156,9 +157,14 @@ contract StakingManager is return poolConfig[_pool].enabled; } + function getPoolConfig(address _pool) external view returns (Struct.PoolConfig memory) { + return poolConfig[_pool]; + } + /*======================================== Admin ========================================*/ - // add new staking pool + /// @notice add new staking pool + /// @dev share and enabled must be set function addStakingPool(address _stakingPool) external onlyRole(DEFAULT_ADMIN_ROLE) { stakingPoolSet.add(_stakingPool); @@ -171,6 +177,12 @@ contract StakingManager is // TODO: emit event } + function setEnabledPool(address _pool, bool _enabled) external onlyRole(DEFAULT_ADMIN_ROLE) { + poolConfig[_pool].enabled = _enabled; + + // TODO: emit event + } + function setJobManager(address _jobManager) external onlyRole(DEFAULT_ADMIN_ROLE) { jobManager = _jobManager; @@ -178,7 +190,7 @@ contract StakingManager is } // when job is closed, the reward will be distributed based on the share - function setPoolShare(address[] calldata _pools, uint256[] calldata _shares) + function setPoolRewardShare(address[] calldata _pools, uint256[] calldata _shares) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 0f04124..81c1c37 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -40,8 +40,8 @@ contract SymbioticStaking is bytes32 public constant STAKE_SNAPSHOT_TYPE = keccak256("STAKE_SNAPSHOT"); - uint256 submissionCooldown; // 18 decimal (in seconds) - uint256 baseTransmitterComissionRate; // 18 decimal (in percentage) + uint256 public submissionCooldown; // 18 decimal (in seconds) + uint256 public baseTransmitterComissionRate; // 18 decimal (in percentage) bytes32 public constant SLASH_RESULT_TYPE = keccak256("SLASH_RESULT"); @@ -71,6 +71,7 @@ contract SymbioticStaking is mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; // to track if all partial txs are received mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; + // staked amount for each operator mapping(uint256 captureTimestamp => mapping(address operator => mapping(address stakeToken => uint256 stakeAmount))) operatorStakeAmounts; // staked amount for each vault @@ -188,7 +189,7 @@ contract SymbioticStaking is /*--------------------------- stake lock/unlock for job --------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectStakeToken(); + address _token = _selectStakeToken(_operator); uint256 _amountToLock = amountToLock[_token]; require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake amount"); @@ -403,7 +404,7 @@ contract SymbioticStaking is /*-------------------------------------- Job -------------------------------------*/ - function _selectStakeToken() internal view returns(address) { + function _selectStakeToken(address _operator) internal view returns(address) { require(tokenSelectionWeightSum > 0, "Total weight must be greater than zero"); require(stakeTokenSet.length() > 0, "No tokens available"); @@ -447,7 +448,7 @@ contract SymbioticStaking is } // check if the selected token has enough active stake amount - if (_getTotalActiveStakeAmount(selectedToken) >= amountToLock[selectedToken]) { + if (getOperatorActiveStakeAmount(_operator, selectedToken) >= amountToLock[selectedToken]) { return selectedToken; } @@ -461,12 +462,13 @@ contract SymbioticStaking is return address(0); } - function _getTotalActiveStakeAmount(address _stakeToken) internal view returns(uint256) { + function _getActiveStakeAmount(address _stakeToken) internal view returns(uint256) { // TODO } function _transmitterComissionRate(uint256 _lastConfirmedTimestamp) internal view returns (uint256) { // TODO: implement logic + return baseTransmitterComissionRate; } /*------------------------------------ Reward ------------------------------------*/ @@ -497,13 +499,22 @@ contract SymbioticStaking is delete inflationRewardShare[_token]; } + function setStakeTokenWeight(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + tokenSelectionWeightSum -= tokenSelectionWeight[_token]; + tokenSelectionWeight[_token] = _weight; + tokenSelectionWeightSum += _weight; + } + function setSubmissionCooldown(uint256 _submissionCooldown) external onlyRole(DEFAULT_ADMIN_ROLE) { submissionCooldown = _submissionCooldown; // TODO: emit event } + + /// @dev base transmitter comission rate is in range [0, 1e18) + function setBaseTransmitterComissionRate(uint256 _baseTransmitterComission) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_baseTransmitterComission < 1e18, "Invalid comission rate"); - function setBaseTransmitterComission(uint256 _baseTransmitterComission) external onlyRole(DEFAULT_ADMIN_ROLE) { baseTransmitterComissionRate = _baseTransmitterComission; // TODO: emit event From ae2ac28f5310510a1fefc7390cd423afe9131de1 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Fri, 11 Oct 2024 14:39:42 +0900 Subject: [PATCH 155/158] test operator self stake --- test/foundry/TestSetup.t.sol | 9 ++- test/foundry/e2e/KalypsoStaking.t.sol | 95 +++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 test/foundry/e2e/KalypsoStaking.t.sol diff --git a/test/foundry/TestSetup.t.sol b/test/foundry/TestSetup.t.sol index a9e4a01..6c4af4d 100644 --- a/test/foundry/TestSetup.t.sol +++ b/test/foundry/TestSetup.t.sol @@ -41,6 +41,7 @@ contract TestSetup is Test { uint256 constant FUND_FOR_GAS = 10 ether; // 10 ether uint256 constant FUND_FOR_FEE = 10_000 ether; // 10,000 USDC + uint256 constant FUND_FOR_SELF_STAKE = 1000_000 ether; // 10,000 POND uint256 constant FUND_FOR_INFLATION_REWARD = 100_000 ether; // 100,000 POND uint256 constant INFLATION_REWARD_EPOCH_SIZE = 30 minutes; // 30 minutes @@ -283,7 +284,7 @@ contract TestSetup is Test { function _setNativeStakingConfig() internal { vm.startPrank(admin); - NativeStaking(nativeStaking).setStakeToken(pond, true); + NativeStaking(nativeStaking).addStakeToken(pond, _calcShareAmount(HUNDRED_PERCENT)); vm.stopPrank(); } @@ -303,6 +304,12 @@ contract TestSetup is Test { assertEq(SymbioticStaking(symbioticStaking).submissionCooldown(), 12 hours); } + function _fund_tokens() internal { + deal(pond, operatorA, FUND_FOR_SELF_STAKE); + deal(pond, operatorB, FUND_FOR_SELF_STAKE); + deal(pond, operatorC, FUND_FOR_SELF_STAKE); + } + /*===================================== internal pure ======================================*/ diff --git a/test/foundry/e2e/KalypsoStaking.t.sol b/test/foundry/e2e/KalypsoStaking.t.sol new file mode 100644 index 0000000..14ea5c8 --- /dev/null +++ b/test/foundry/e2e/KalypsoStaking.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {TestSetup} from "../TestSetup.t.sol"; + +/* contracts */ +import {JobManager} from "../../../contracts/staking/l2_contracts/JobManager.sol"; +import {StakingManager} from "../../../contracts/staking/l2_contracts/StakingManager.sol"; +import {SymbioticStaking} from "../../../contracts/staking/l2_contracts/SymbioticStaking.sol"; + +/* interfaces */ +import {IJobManager} from "../../../contracts/interfaces/staking/IJobManager.sol"; +import {IStakingManager} from "../../../contracts/interfaces/staking/IStakingManager.sol"; +import {INativeStaking} from "../../../contracts/interfaces/staking/INativeStaking.sol"; +import {ISymbioticStaking} from "../../../contracts/interfaces/staking/ISymbioticStaking.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* libraries */ +import {Struct} from "../../../contracts/lib/staking/Struct.sol"; + +contract KalypsoStakingTest is Test, TestSetup { + + uint256 constant OPERATORA_SELF_STAKE_AMOUNT = 1000 ether; + uint256 constant OPERATORB_SELF_STAKE_AMOUNT = 2000 ether; + + function setUp() public { + _setupAddr(); + _setupContracts(); + _fund_tokens(); + + /*-------------------- Config --------------------*/ + /* JobManager */ + _setJobManagerConfig(); + + /* StakingManager */ + _setStakingManagerConfig(); + + /* NativeStaking */ + _setNativeStakingConfig(); + + /* SymbioticStaking */ + _setSymbioticStakingConfig(); + + } + + /// @notice test full lifecycle of kalypso staking + function test_kalypso_staking() public { + /*-------------------- Native Staking Stake --------------------*/ + + _operator_self_stake(); + + // _symbiotic_staking_snapshot_transmission(); + } + + function _operator_self_stake() internal { + // Operator A self stakes into 1_000 POND + vm.startPrank(operatorA); + { + IERC20(weth).approve(nativeStaking, type(uint256).max); + IERC20(pond).approve(nativeStaking, type(uint256).max); + + // weth is not supported in NativeStaking + vm.expectRevert("Token not supported"); + INativeStaking(nativeStaking).stake(operatorA, weth, OPERATORA_SELF_STAKE_AMOUNT); + + // only operator can stake + vm.expectRevert("Only operator can stake"); + INativeStaking(nativeStaking).stake(operatorB, pond, OPERATORA_SELF_STAKE_AMOUNT); + + // stake 1000 POND + INativeStaking(nativeStaking).stake(operatorA, pond, OPERATORA_SELF_STAKE_AMOUNT); + } + vm.stopPrank(); + assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(operatorA, pond), OPERATORA_SELF_STAKE_AMOUNT); + assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(operatorA, pond), OPERATORA_SELF_STAKE_AMOUNT); + + vm.startPrank(operatorB); + { + IERC20(pond).approve(nativeStaking, type(uint256).max); + + INativeStaking(nativeStaking).stake(operatorB, pond, OPERATORB_SELF_STAKE_AMOUNT); + + } + vm.stopPrank(); + assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(operatorB, pond), OPERATORB_SELF_STAKE_AMOUNT); + assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(operatorB, pond), OPERATORB_SELF_STAKE_AMOUNT); + } + + function _symbiotic_staking_snapshot_submission() internal { + + } +} From d1fba227b7a08081db0241ec91ec99544ea25ef8 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 14 Oct 2024 14:10:15 +0900 Subject: [PATCH 156/158] fix unexpected logic --- .../staking/IInflationRewardManager.sol | 2 + .../interfaces/staking/INativeStaking.sol | 2 +- contracts/interfaces/staking/IStakingPool.sol | 20 +- .../interfaces/staking/ISymbioticStaking.sol | 26 +- .../staking/ISymbioticStakingReward.sol | 4 + .../l2_contracts/InflationRewardManger.sol | 22 +- contracts/staking/l2_contracts/JobManager.sol | 29 +- .../staking/l2_contracts/NativeStaking.sol | 100 +++--- .../staking/l2_contracts/StakingManager.sol | 14 +- .../staking/l2_contracts/SymbioticStaking.sol | 319 +++++++++++------- .../l2_contracts/SymbioticStakingReward.sol | 92 +++-- 11 files changed, 410 insertions(+), 220 deletions(-) diff --git a/contracts/interfaces/staking/IInflationRewardManager.sol b/contracts/interfaces/staking/IInflationRewardManager.sol index 8dab1f5..25da537 100644 --- a/contracts/interfaces/staking/IInflationRewardManager.sol +++ b/contracts/interfaces/staking/IInflationRewardManager.sol @@ -5,4 +5,6 @@ interface IInflationRewardManager { function updatePendingInflationReward(address _operator) external returns (uint256 timestampIdx, uint256 pendingInflationReward); function updateEpochTimestampIdx() external; + + function transferInflationRewardToken(address _to, uint256 _amount) external; } \ No newline at end of file diff --git a/contracts/interfaces/staking/INativeStaking.sol b/contracts/interfaces/staking/INativeStaking.sol index 9f260ed..2c586ff 100644 --- a/contracts/interfaces/staking/INativeStaking.sol +++ b/contracts/interfaces/staking/INativeStaking.sol @@ -5,7 +5,7 @@ import {IStakingPool} from "../staking/IStakingPool.sol"; interface INativeStaking is IStakingPool { - function stake(address operator, address stakeToken, uint256 amount) external; + function stake(address stakeToken, address operator, uint256 amount) external; // TODO: check if timestamp is needed event Staked(address indexed account, address indexed operator, address indexed token, uint256 amount, uint256 timestamp); diff --git a/contracts/interfaces/staking/IStakingPool.sol b/contracts/interfaces/staking/IStakingPool.sol index ca26f52..cf743b3 100644 --- a/contracts/interfaces/staking/IStakingPool.sol +++ b/contracts/interfaces/staking/IStakingPool.sol @@ -4,23 +4,27 @@ pragma solidity ^0.8.26; import {Struct} from "../../lib/staking/Struct.sol"; interface IStakingPool { - function isSupportedStakeToken(address _token) external view returns (bool); + function isSupportedStakeToken(address stakeToken) external view returns (bool); - function lockStake(uint256 _jobId, address _operator) external; // Staking Manager only + function lockStake(uint256 jobId, address operator) external; // Staking Manager only - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount, uint256 _timestampIdx) external; // Staking Manager only + function onJobCompletion(uint256 jobId, address operator, uint256 feeRewardAmount, uint256 inflationRewardAmount, uint256 timestampIdx) external; // Staking Manager only - function slash(Struct.JobSlashed[] calldata _slashedJobs) external; // Staking Manager only + function slash(Struct.JobSlashed[] calldata slashedJobs) external; // Staking Manager only - function getOperatorStakeAmount(address _operator, address _token) external view returns (uint256); + function getOperatorStakeAmount(address stakeToken, address operator) external view returns (uint256); - function getOperatorActiveStakeAmount(address _operator, address _token) external view returns (uint256); + function getOperatorActiveStakeAmount(address stakeToken, address operator) external view returns (uint256); function rewardDistributor() external view returns (address); - function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) external; // Staking Manager only + function distributeInflationReward(address operator, uint256 rewardAmount, uint256 timestampIdx) external; // Staking Manager only function getStakeTokenList() external view returns (address[] memory); - function getStakeAmount(address staker, address stakeToken, address operator) external view returns (uint256); + function getStakeTokenWeights() external view returns (address[] memory, uint256[] memory); + + function tokenSelectionWeightSum() external view returns (uint256); + + function getStakeAmount(address stakeToken, address staker, address operator) external view returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/staking/ISymbioticStaking.sol b/contracts/interfaces/staking/ISymbioticStaking.sol index ec78e83..2179f1b 100644 --- a/contracts/interfaces/staking/ISymbioticStaking.sol +++ b/contracts/interfaces/staking/ISymbioticStaking.sol @@ -3,8 +3,30 @@ pragma solidity ^0.8.26; import {IStakingPool} from "./IStakingPool.sol"; +import {Struct} from "../../lib/staking/Struct.sol"; + interface ISymbioticStaking is IStakingPool { - // function stakeOf(address _operator, address _token) external view returns (uint256); + function submitVaultSnapshot( + uint256 _index, + uint256 _numOfTxs, // number of total transactions + bytes calldata _vaultSnapshotData, + bytes calldata _signature + ) external; + + function submitSlashResult( + uint256 _index, + uint256 _numOfTxs, // number of total transactions + bytes memory _slashResultData, + bytes memory _signature + ) external; + + function getTxCountInfo(uint256 _captureTimestamp, address _transmitter, bytes32 _type) external view returns (Struct.SnapshotTxCountInfo memory); + + function getSubmissionStatus(uint256 _captureTimestamp, address _transmitter) external view returns (bytes32); + + + + function confirmedTimestampInfo(uint256 _idx) external view returns (Struct.ConfirmedTimestamp memory); // event OperatorSnapshotSubmitted @@ -19,4 +41,4 @@ interface ISymbioticStaking is IStakingPool { /// @notice Returns the timestampIdx of latest completed snapshot submission function latestConfirmedTimestampIdx() external view returns (uint256); -} \ No newline at end of file +} diff --git a/contracts/interfaces/staking/ISymbioticStakingReward.sol b/contracts/interfaces/staking/ISymbioticStakingReward.sol index b7fc905..da9c7b8 100644 --- a/contracts/interfaces/staking/ISymbioticStakingReward.sol +++ b/contracts/interfaces/staking/ISymbioticStakingReward.sol @@ -5,9 +5,13 @@ import {Struct} from "../../lib/staking/Struct.sol"; pragma solidity ^0.8.26; interface ISymbioticStakingReward { + function claimReward(address _operator) external; + function updateFeeReward(address _stakeToken, address _operator, uint256 _amount) external; function updateInflationReward(address _operator, uint256 _rewardAmount) external; function onSnapshotSubmission(Struct.VaultSnapshot[] calldata _vaultSnapshots) external; + + function onSnapshotSubmission(address _vault, address _operator) external; } \ No newline at end of file diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 818d936..2374b3c 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -16,6 +16,8 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {console} from "forge-std/Test.sol"; + contract InflationRewardManager is ContextUpgradeable, ERC165Upgradeable, @@ -36,6 +38,7 @@ contract InflationRewardManager is address public jobManager; address public stakingManager; address public symbioticStaking; + address public symbioticStakingReward; /* reward config */ address public inflationRewardToken; @@ -61,7 +64,7 @@ contract InflationRewardManager is } modifier onlySymbioticStaking() { - require(msg.sender == symbioticStaking, "InflationRewardManager: Only SymbioticStaking"); + require(msg.sender == symbioticStaking || msg.sender == symbioticStakingReward, "InflationRewardManager: Only SymbioticStaking or SymbioticStakingReward"); _; } @@ -72,6 +75,8 @@ contract InflationRewardManager is uint256 _startTime, address _jobManager, address _stakingManager, + address _symbioticStaking, + address _symbioticStakingReward, address _inflationRewardToken, uint256 _inflationRewardEpochSize, uint256 _inflationRewardPerEpoch @@ -89,6 +94,12 @@ contract InflationRewardManager is require(_stakingManager != address(0), "InflationRewardManager: stakingManager address is zero"); stakingManager = _stakingManager; + require(_symbioticStaking != address(0), "InflationRewardManager: symbioticStaking address is zero"); + symbioticStaking = _symbioticStaking; + + require(_symbioticStakingReward != address(0), "InflationRewardManager: symbioticStakingReward address is zero"); + symbioticStakingReward = _symbioticStakingReward; + require(_startTime > 0, "InflationRewardManager: startTime is zero"); startTime = _startTime; @@ -214,6 +225,15 @@ contract InflationRewardManager is inflationRewardEpochSize = _inflationRewardEpochSize; } + function setSymbioticStakingReward(address _symbioticStakingReward) public onlyRole(DEFAULT_ADMIN_ROLE) { + require(_symbioticStakingReward != address(0), "InflationRewardManager: symbioticStakingReward address is zero"); + symbioticStakingReward = _symbioticStakingReward; + } + + function transferInflationRewardToken(address _to, uint256 _amount) public onlySymbioticStaking { + IERC20(inflationRewardToken).safeTransfer(_to, _amount); + } + /*======================================== Overrides ========================================*/ function supportsInterface(bytes4 interfaceId) diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index ff1bbfe..240a5af 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -39,6 +39,8 @@ contract JobManager is uint256[500] private __gap_0; address public stakingManager; + address public symbioticStaking; + address public symbioticStakingReward; address public feeToken; address public inflationRewardManager; @@ -47,9 +49,14 @@ contract JobManager is // gaps in case we new vars in same file uint256[500] private __gap_1; + modifier onlySymbioticStaking() { + require(msg.sender == symbioticStaking || msg.sender == symbioticStakingReward, "JobManager: caller is not the SymbioticStaking"); + _; + } + /*======================================== Init ========================================*/ - function initialize(address _admin, address _stakingManager, address _feeToken, address _inflationRewardManager, uint256 _jobDuration) + function initialize(address _admin, address _stakingManager, address _symbioticStaking, address _symbioticStakingReward, address _feeToken, address _inflationRewardManager, uint256 _jobDuration) public initializer { @@ -63,8 +70,15 @@ contract JobManager is require(_stakingManager != address(0), "JobManager: Invalid StakingManager"); stakingManager = _stakingManager; + require(_symbioticStaking != address(0), "JobManager: Invalid SymbioticStaking"); + symbioticStaking = _symbioticStaking; + + require(_symbioticStakingReward != address(0), "JobManager: Invalid SymbioticStakingReward"); + symbioticStakingReward = _symbioticStakingReward; + require(_feeToken != address(0), "JobManager: Invalid Fee Token"); feeToken = _feeToken; + require(_inflationRewardManager != address(0), "JobManager: Invalid InflationRewardManager"); inflationRewardManager = _inflationRewardManager; @@ -100,11 +114,12 @@ contract JobManager is * @notice Submit Single Proof */ function submitProof(uint256 _jobId, bytes calldata _proof) public nonReentrant { + require(jobs[_jobId].deadline > 0, "Job not created"); require(block.timestamp <= jobs[_jobId].deadline, "Job Expired"); _verifyProof(_jobId, _proof); - - address operator = jobs[_jobId].operator; + + address operator = jobs[_jobId].operator; // distribute fee reward uint256 feeRewardRemaining = _distributeFeeReward(operator, jobs[_jobId].feePaid); @@ -135,9 +150,8 @@ contract JobManager is if (jobs[_jobId].feePaid > 0) { require(block.timestamp > jobs[_jobId].deadline, "Job not Expired"); - jobs[_jobId].feePaid = 0; - IERC20(feeToken).safeTransfer(jobs[_jobId].requester, jobs[_jobId].feePaid); + jobs[_jobId].feePaid = 0; // TODO: emit event } @@ -175,6 +189,11 @@ contract JobManager is operatorRewardShares[_operator] = _rewardShare; } + // TODO + function transferFeeToken(address _recipient, uint256 _amount) external onlySymbioticStaking { + IERC20(feeToken).safeTransfer(_recipient, _amount); + } + /*======================================== Overrides ========================================*/ function supportsInterface(bytes4 interfaceId) diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index 0dfa2a5..e1b4575 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -52,10 +52,10 @@ contract NativeStaking is /* Stake */ // staked amount for each account - mapping(address account => mapping(address operator => mapping(address stakeToken => uint256 amount))) public + mapping(address stakeToken => mapping(address account => mapping(address operator => uint256 amount))) public stakeAmounts; // total staked amounts for each operator - mapping(address operator => mapping(address stakeToken => uint256 stakeAmounts)) public operatorstakeAmounts; + mapping(address stakeToken => mapping(address operator => uint256 amount)) public operatorstakeAmounts; mapping(address account => mapping(address operator => Struct.WithdrawalRequest[] withdrawalRequest)) public withdrawalRequests; @@ -63,7 +63,7 @@ contract NativeStaking is uint256[500] private __gap_1; /* Locked Stakes */ mapping(uint256 jobId => Struct.NativeStakingLock lock) public lockInfo; - mapping(address operator => mapping(address token => uint256 stakeAmounts)) public operatorLockedAmounts; + mapping(address stakeToken => mapping(address operator => uint256 amount)) public operatorLockedAmounts; modifier onlySupportedToken(address _stakeToken) { require(stakeTokenSet.contains(_stakeToken), "Token not supported"); @@ -104,7 +104,7 @@ contract NativeStaking is /*-------------------------------- Native Staking --------------------------------*/ // Staker should be able to choose an Operator they want to stake into - function stake(address _operator, address _stakeToken, uint256 _amount) + function stake(address _stakeToken, address _operator, uint256 _amount) external onlySupportedToken(_stakeToken) nonReentrant @@ -114,21 +114,20 @@ contract NativeStaking is IERC20(_stakeToken).safeTransferFrom(msg.sender, address(this), _amount); - stakeAmounts[msg.sender][_operator][_stakeToken] += _amount; - operatorstakeAmounts[_operator][_stakeToken] += _amount; + stakeAmounts[_stakeToken][msg.sender][_operator] += _amount; + operatorstakeAmounts[_stakeToken][_operator] += _amount; // INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, _stakeToken, _operator); emit Staked(msg.sender, _operator, _stakeToken, _amount, block.timestamp); } - // This should update StakingManger's state // TODO function requestStakeWithdrawal(address _operator, address _stakeToken, uint256 _amount) external nonReentrant { - require(getOperatorActiveStakeAmount(_operator, _stakeToken) >= _amount, "Insufficient stake"); + require(getOperatorActiveStakeAmount(_stakeToken, _operator) >= _amount, "Insufficient stake"); - stakeAmounts[msg.sender][_operator][_stakeToken] -= _amount; - operatorstakeAmounts[_operator][_stakeToken] -= _amount; + stakeAmounts[_stakeToken][msg.sender][_operator] -= _amount; + operatorstakeAmounts[_stakeToken][_operator] -= _amount; withdrawalRequests[msg.sender][_operator].push( Struct.WithdrawalRequest(_stakeToken, _amount, block.timestamp + withdrawalDuration) @@ -149,13 +148,13 @@ contract NativeStaking is /*-------------------------------- Satking Manager -------------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectStakeToken(_operator); - uint256 _amountToLock = amountToLock[_token]; - require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake to lock"); + address _stakeToken = _selectStakeToken(_operator); + uint256 _amountToLock = amountToLock[_stakeToken]; + require(getOperatorActiveStakeAmount(_stakeToken, _operator) >= _amountToLock, "Insufficient stake to lock"); // lock stake - lockInfo[_jobId] = Struct.NativeStakingLock(_token, _amountToLock); - operatorLockedAmounts[_operator][_token] += _amountToLock; + lockInfo[_jobId] = Struct.NativeStakingLock(_stakeToken, _amountToLock); + operatorLockedAmounts[_stakeToken][_operator] += _amountToLock; // TODO: emit event } @@ -173,7 +172,7 @@ contract NativeStaking is if (lock.amount == 0) return; - _unlockStake(_jobId, _operator, lock.token, lock.amount); + _unlockStake(_jobId, lock.token, _operator, lock.amount); // distribute fee reward // if (_feeRewardAmount > 0) { @@ -195,7 +194,7 @@ contract NativeStaking is uint256 lockedAmount = lock.amount; if (lockedAmount == 0) continue; // if already slashed - _unlockStake(_slashedJobs[i].jobId, _slashedJobs[i].operator, lock.token, lockedAmount); + _unlockStake(_slashedJobs[i].jobId, lock.token, _slashedJobs[i].operator, lockedAmount); IERC20(lock.token).safeTransfer(_slashedJobs[i].rewardAddress, lockedAmount); // INativeStakingReward(rewardDistributor).onStakeUpdate(msg.sender, lock.token, _slashedJobs[i].operator); @@ -211,12 +210,16 @@ contract NativeStaking is /*==================================================== public view ==================================================*/ - function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorstakeAmounts[_operator][_token]; + function getOperatorStakeAmount(address _stakeToken, address _operator) public view returns (uint256) { + return operatorstakeAmounts[_stakeToken][_operator]; } - function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; + function getOperatorActiveStakeAmount(address _stakeToken, address _operator) public view returns (uint256) { + return getOperatorStakeAmount(_stakeToken, _operator) - getOperatorLockedAmount(_stakeToken, _operator); + } + + function getOperatorLockedAmount(address _stakeToken, address _operator) public view returns (uint256) { + return operatorLockedAmounts[_stakeToken][_operator]; } /*================================================== external view ==================================================*/ @@ -225,37 +228,26 @@ contract NativeStaking is return stakeTokenSet.values(); } - function getStakeAmount(address _account, address _operator, address _stakeToken) external view returns (uint256) { - return stakeAmounts[_account][_operator][_stakeToken]; + function getStakeTokenWeights() external view returns (address[] memory, uint256[] memory) { + uint256[] memory weights = new uint256[](stakeTokenSet.length()); + for (uint256 i = 0; i < stakeTokenSet.length(); i++) { + weights[i] = tokenSelectionWeight[stakeTokenSet.at(i)]; + } + return (stakeTokenSet.values(), weights); + } + + function getStakeAmount(address _stakeToken, address _account, address _operator) external view returns (uint256) { + return stakeAmounts[_stakeToken][_account][_operator]; } - function isSupportedStakeToken(address _token) public view returns (bool) { - return stakeTokenSet.contains(_token); + function isSupportedStakeToken(address _stakeToken) public view returns (bool) { + return stakeTokenSet.contains(_stakeToken); } /*===================================================== internal ====================================================*/ - // function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - // IERC20(feeRewardToken).safeTransfer(rewardDistributor, _amount); - // IRewardDistributor(rewardDistributor).addFeeReward(_stakeToken, _operator, _amount); - // } - - // function _distributeInflationReward(address _operator, uint256 _rewardAmount) internal { - // uint256 len = stakeTokenSet.length(); - // address[] memory stakeTokens = stakeTokenSet.values(); - // uint256[] memory rewardAmounts = new uint256[](len); - // uint256 inflationRewardAmount; - // for (uint256 i = 0; i < len; i++) { - // rewardAmounts[i] = _calcInflationRewardAmount(stakeTokens[i], _rewardAmount); - // inflationRewardAmount += rewardAmounts[i]; - // } - - // IERC20(inflationRewardToken).safeTransfer(rewardDistributor, inflationRewardAmount); - // IRewardDistributor(rewardDistributor).addInflationReward(_operator, stakeTokens, rewardAmounts); - // } - - function _unlockStake(uint256 _jobId, address _operator, address _stakeToken, uint256 _amount) internal { - operatorLockedAmounts[_operator][_stakeToken] -= _amount; + function _unlockStake(uint256 _jobId, address _stakeToken, address _operator, uint256 _amount) internal { + operatorLockedAmounts[_stakeToken][_operator] -= _amount; delete lockInfo[_jobId]; } @@ -291,7 +283,6 @@ contract NativeStaking is uint256[] memory weights = new uint256[](stakeTokenSet.length()); uint256 weightSum = tokenSelectionWeightSum; - uint256 idx = 0; uint256 len = stakeTokenSet.length(); for (uint256 i = 0; i < len; i++) { @@ -315,7 +306,6 @@ contract NativeStaking is uint256 cumulativeWeight = 0; address selectedToken; - uint256 i; // select token based on weight for (i = 0; i < idx; i++) { @@ -327,7 +317,7 @@ contract NativeStaking is } // check if the selected token has enough active stake amount - if (getOperatorActiveStakeAmount(_operator, selectedToken) >= amountToLock[selectedToken]) { + if (getOperatorActiveStakeAmount(selectedToken, _operator) >= amountToLock[selectedToken]) { return selectedToken; } @@ -341,28 +331,20 @@ contract NativeStaking is return address(0); } - // function _getOperatorStakeAmount(address _operator, address _token) internal view returns (uint256) { - // return operatorstakeAmounts[_operator][_token]; - // } - - // function _getOperatorActiveStakeAmount(address _operator, address _token) internal view returns (uint256) { - // return operatorstakeAmounts[_operator][_token] - operatorLockedAmounts[_operator][_token]; - // } - /*====================================================== admin ======================================================*/ function addStakeToken(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { require(stakeTokenSet.add(_token), "Token already exists"); + tokenSelectionWeight[_token] = _weight; tokenSelectionWeightSum += _weight; - inflationRewardShare[_token] = _weight; } function removeStakeToken(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) { require(stakeTokenSet.remove(_token), "Token does not exist"); tokenSelectionWeightSum -= inflationRewardShare[_token]; - delete inflationRewardShare[_token]; + delete tokenSelectionWeight[_token]; } function setStakeTokenWeight(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index cad3588..a8facd4 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -38,11 +38,13 @@ contract StakingManager is EnumerableSet.AddressSet private stakingPoolSet; address public jobManager; + address public symbioticStaking; address public inflationRewardManager; address public feeToken; address public inflationRewardToken; + // gaps in case we new vars in same file uint256[500] private __gap_1; @@ -51,12 +53,17 @@ contract StakingManager is _; } + modifier onlySymbioticStaking() { + require(msg.sender == symbioticStaking, "StakingManager: Only SymbioticStaking"); + _; + } + modifier onlyInflationRewardManager() { require(msg.sender == jobManager, "StakingManager: Only JobManager"); _; } - function initialize(address _admin, address _jobManager, address _inflationRewardManager, address _feeToken, address _inflationRewardToken) public initializer { + function initialize(address _admin, address _jobManager, address _symbioticStaking, address _inflationRewardManager, address _feeToken, address _inflationRewardToken) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -73,6 +80,9 @@ contract StakingManager is require(_feeToken != address(0), "StakingManager: Invalid FeeToken"); feeToken = _feeToken; + require(_symbioticStaking != address(0), "StakingManager: Invalid SymbioticStaking"); + symbioticStaking = _symbioticStaking; + require(_inflationRewardToken != address(0), "StakingManager: Invalid InflationRewardToken"); inflationRewardToken = _inflationRewardToken; } @@ -112,7 +122,7 @@ contract StakingManager is } /// @notice called by SymbioticStaking contract when slash result is submitted - function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlyJobManager { + function onSlashResult(Struct.JobSlashed[] calldata _jobsSlashed) external onlySymbioticStaking { // msg.sender will most likely be SymbioticStaking contract require(stakingPoolSet.contains(msg.sender), "StakingManager: Invalid Pool"); diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 81c1c37..324cea2 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -6,7 +6,7 @@ import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/intro import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; - +import {JobManager} from "./JobManager.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; @@ -19,14 +19,15 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; -contract SymbioticStaking is + +contract SymbioticStaking is ContextUpgradeable, ERC165Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable, - ISymbioticStaking - { + ISymbioticStaking +{ using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -38,16 +39,16 @@ contract SymbioticStaking is bytes32 public constant SLASH_RESULT_MASK = 0x0000000000000000000000000000000000000000000000000000000000000010; bytes32 public constant COMPLETE_MASK = 0x0000000000000000000000000000000000000000000000000000000000000011; - bytes32 public constant STAKE_SNAPSHOT_TYPE = keccak256("STAKE_SNAPSHOT"); + bytes32 public constant STAKE_SNAPSHOT_TYPE = keccak256("STAKE_SNAPSHOT_TYPE"); + bytes32 public constant SLASH_RESULT_TYPE = keccak256("SLASH_RESULT_TYPE"); uint256 public submissionCooldown; // 18 decimal (in seconds) uint256 public baseTransmitterComissionRate; // 18 decimal (in percentage) - bytes32 public constant SLASH_RESULT_TYPE = keccak256("SLASH_RESULT"); - EnumerableSet.AddressSet stakeTokenSet; address public stakingManager; + address public jobManager; address public rewardDistributor; address public inflationRewardManager; @@ -58,31 +59,35 @@ contract SymbioticStaking is // gaps in case we new vars in same file uint256[500] private __gap_1; - - + /* Config */ mapping(address stakeToken => uint256 amount) public amountToLock; mapping(address stakeToken => uint256 weight) public tokenSelectionWeight; mapping(address stakeToken => uint256 share) public inflationRewardShare; // 1e18 = 100% - /* Symbiotic Snapshot */ // to track if all partial txs are received - mapping(uint256 captureTimestamp => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot))) txCountInfo; + mapping( + uint256 captureTimestamp + => mapping(address account => mapping(bytes32 submissionType => Struct.SnapshotTxCountInfo snapshot)) + ) txCountInfo; // to track if all partial txs are received - mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; + mapping(uint256 captureTimestamp => mapping(address account => bytes32 status)) submissionStatus; // staked amount for each operator - mapping(uint256 captureTimestamp => mapping(address operator => mapping(address stakeToken => uint256 stakeAmount))) operatorStakeAmounts; + mapping(uint256 captureTimestamp => mapping(address stakeToken => mapping(address operator => uint256 stakeAmount))) + operatorStakeAmounts; // staked amount for each vault - mapping(uint256 captureTimestamp => mapping(address vault => mapping(address operator => mapping(address stakeToken => uint256 stakeAmount)))) vaultStakeAmounts; + mapping( + uint256 captureTimestamp + => mapping(address stakeToken => mapping(address vault => mapping(address operator => uint256 stakeAmount))) + ) vaultStakeAmounts; Struct.ConfirmedTimestamp[] public confirmedTimestamps; // timestamp is added once all types of partial txs are received - /* Staking */ mapping(uint256 jobId => Struct.SymbioticStakingLock lockInfo) public lockInfo; // note: this does not actually affect L1 Symbiotic stake - mapping(address operator => mapping(address stakeToken => uint256 locked)) public operatorLockedAmounts; + mapping(address stakeToken => mapping(address operator => uint256 locked)) public operatorLockedAmounts; mapping(uint256 captureTimestamp => address transmitter) registeredTransmitters; // only one transmitter can submit the snapshot for the same capturetimestamp @@ -91,7 +96,15 @@ contract SymbioticStaking is _; } - function initialize(address _admin, address _stakingManager, address _rewardDistributor, address _inflationRewardManager, address _feeRewardToken, address _inflationRewardToken) public initializer { + function initialize( + address _admin, + address _jobManager, + address _stakingManager, + address _rewardDistributor, + address _inflationRewardManager, + address _feeRewardToken, + address _inflationRewardToken + ) public initializer { __Context_init_unchained(); __ERC165_init_unchained(); __AccessControl_init_unchained(); @@ -103,6 +116,9 @@ contract SymbioticStaking is require(_stakingManager != address(0), "SymbioticStaking: stakingManager is zero"); stakingManager = _stakingManager; + require(_jobManager != address(0), "SymbioticStaking: jobManager is zero"); + jobManager = _jobManager; + require(_rewardDistributor != address(0), "SymbioticStaking: rewardDistributor is zero"); rewardDistributor = _rewardDistributor; @@ -115,38 +131,37 @@ contract SymbioticStaking is require(_inflationRewardToken != address(0), "SymbioticStaking: inflationRewardToken is zero"); inflationRewardToken = _inflationRewardToken; } - - /*===================================================== external ====================================================*/ + /*===================================================== external ====================================================*/ /*------------------------------ L1 to L2 submission -----------------------------*/ function submitVaultSnapshot( uint256 _index, uint256 _numOfTxs, // number of total transactions - uint256 _captureTimestamp, - bytes memory _vaultSnapshotData, - bytes memory _signature + bytes calldata _vaultSnapshotData, + bytes calldata _signature ) external { - _checkTransmitterRegistration(_captureTimestamp); + (uint256 captureTimestamp, Struct.VaultSnapshot[] memory _vaultSnapshots) = + abi.decode(_vaultSnapshotData, (uint256, Struct.VaultSnapshot[])); - _checkValidity(_index, _numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); + _checkTransmitterRegistration(captureTimestamp); - _verifySignature(_index, _numOfTxs, _captureTimestamp, _vaultSnapshotData, _signature); - - Struct.VaultSnapshot[] memory _vaultSnapshots = abi.decode(_vaultSnapshotData, (Struct.VaultSnapshot[])); + _checkValidity(_index, _numOfTxs, captureTimestamp, STAKE_SNAPSHOT_TYPE); + + _verifySignature(_index, _numOfTxs, captureTimestamp, _vaultSnapshotData, _signature); // update Vault and Operator stake amount // update rewardPerToken for each vault and operator in SymbioticStakingReward - _submitVaultSnapshot(_captureTimestamp, _vaultSnapshots); + _submitVaultSnapshot(captureTimestamp, _vaultSnapshots); + + _updateTxCountInfo(_numOfTxs, captureTimestamp, STAKE_SNAPSHOT_TYPE); - _updateTxCountInfo(_numOfTxs, _captureTimestamp, STAKE_SNAPSHOT_TYPE); + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; - // when all chunks of VaultSnapshots are submitted if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; + submissionStatus[captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; } } @@ -154,85 +169,103 @@ contract SymbioticStaking is function submitSlashResult( uint256 _index, uint256 _numOfTxs, // number of total transactions - uint256 _captureTimestamp, bytes memory _SlashResultData, bytes memory _signature ) external { + (uint256 captureTimestamp, Struct.JobSlashed[] memory _jobSlashed) = + abi.decode(_SlashResultData, (uint256, Struct.JobSlashed[])); + // Vault Snapshot should be submitted before Slash Result - require(submissionStatus[_captureTimestamp][msg.sender] & STAKE_SNAPSHOT_MASK == STAKE_SNAPSHOT_MASK, "Vault Snapshot not submitted"); + require( + submissionStatus[captureTimestamp][msg.sender] & STAKE_SNAPSHOT_MASK == STAKE_SNAPSHOT_MASK, + "Vault Snapshot not submitted" + ); + + _checkTransmitterRegistration(captureTimestamp); - _checkTransmitterRegistration(_captureTimestamp); + _checkValidity(_index, _numOfTxs, captureTimestamp, SLASH_RESULT_TYPE); - _checkValidity(_index, _numOfTxs, _captureTimestamp, SLASH_RESULT_TYPE); + _verifySignature(_index, _numOfTxs, captureTimestamp, _SlashResultData, _signature); - _verifySignature(_index, _numOfTxs, _captureTimestamp, _SlashResultData, _signature); + _updateTxCountInfo(_numOfTxs, captureTimestamp, SLASH_RESULT_TYPE); - Struct.JobSlashed[] memory _jobSlashed = abi.decode(_SlashResultData, (Struct.JobSlashed[])); - // _updateSlashResultDataInfo(_captureTimestamp, _jobSlashed); + Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; - _updateTxCountInfo(_numOfTxs, _captureTimestamp, SLASH_RESULT_TYPE); + // there could be no operator slashed + if (_jobSlashed.length > 0) IStakingManager(stakingManager).onSlashResult(_jobSlashed); - Struct.SnapshotTxCountInfo memory _snapshot = txCountInfo[_captureTimestamp][msg.sender][STAKE_SNAPSHOT_TYPE]; - - IStakingManager(stakingManager).onSlashResult(_jobSlashed); - // when all chunks of Snapshots are submitted - if (_snapshot.idxToSubmit == _snapshot.numOfTxs) { - submissionStatus[_captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; - _completeSubmission(_captureTimestamp); + uint256 numOfTxsStored = _snapshot.numOfTxs; + if (numOfTxsStored > 0 && _snapshot.idxToSubmit == _snapshot.numOfTxs) { + submissionStatus[captureTimestamp][msg.sender] |= STAKE_SNAPSHOT_MASK; + _completeSubmission(captureTimestamp); } - - // TODO: unlock the selfStake and reward it to the transmitter + // TODO: unlock the selfStake and reward it to the transmitter } /*--------------------------- stake lock/unlock for job --------------------------*/ function lockStake(uint256 _jobId, address _operator) external onlyStakingManager { - address _token = _selectStakeToken(_operator); - uint256 _amountToLock = amountToLock[_token]; - require(getOperatorActiveStakeAmount(_operator, _token) >= _amountToLock, "Insufficient stake amount"); + address _stakeToken = _selectStakeToken(_operator); + uint256 _amountToLock = amountToLock[_stakeToken]; + require(getOperatorActiveStakeAmount(_stakeToken, _operator) >= _amountToLock, "Insufficient stake amount"); - lockInfo[_jobId] = Struct.SymbioticStakingLock(_token, _amountToLock); - operatorLockedAmounts[_operator][_token] += _amountToLock; + lockInfo[_jobId] = Struct.SymbioticStakingLock(_stakeToken, _amountToLock); + operatorLockedAmounts[_stakeToken][_operator] += _amountToLock; // TODO: emit event } - function onJobCompletion(uint256 _jobId, address _operator, uint256 _feeRewardAmount, uint256 _inflationRewardAmount, uint256 _inflationRewardTimestampIdx) external onlyStakingManager { + function onJobCompletion( + uint256 _jobId, + address _operator, + uint256 _feeRewardAmount, + uint256 _inflationRewardAmount, + uint256 _inflationRewardTimestampIdx + ) external onlyStakingManager { Struct.SymbioticStakingLock memory lock = lockInfo[_jobId]; // currentEpoch => currentTimestampIdx IInflationRewardManager(inflationRewardManager).updateEpochTimestampIdx(); // distribute fee reward - if(_feeRewardAmount > 0) { + if (_feeRewardAmount > 0) { uint256 currentTimestampIdx = latestConfirmedTimestampIdx(); - uint256 transmitterComission = Math.mulDiv(_feeRewardAmount, confirmedTimestamps[currentTimestampIdx].transmitterComissionRate, 1e18); + uint256 transmitterComission = + Math.mulDiv(_feeRewardAmount, confirmedTimestamps[currentTimestampIdx].transmitterComissionRate, 1e18); uint256 feeRewardRemaining = _feeRewardAmount - transmitterComission; // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation - IERC20(feeRewardToken).safeTransfer(confirmedTimestamps[currentTimestampIdx].transmitter, transmitterComission); + JobManager(jobManager).transferFeeToken(confirmedTimestamps[currentTimestampIdx].transmitter, transmitterComission); // distribute the remaining fee reward _distributeFeeReward(lock.stakeToken, _operator, feeRewardRemaining); + + ISymbioticStakingReward(rewardDistributor).updateFeeReward(lock.stakeToken, _operator, feeRewardRemaining); } // distribute inflation reward - if(_inflationRewardAmount > 0) { - uint256 transmitterComission = Math.mulDiv(_inflationRewardAmount, confirmedTimestamps[_inflationRewardTimestampIdx].transmitterComissionRate, 1e18); + if (_inflationRewardAmount > 0) { + uint256 transmitterComission = Math.mulDiv( + _inflationRewardAmount, confirmedTimestamps[_inflationRewardTimestampIdx].transmitterComissionRate, 1e18 + ); uint256 inflationRewardRemaining = _inflationRewardAmount - transmitterComission; // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation - IERC20(inflationRewardToken).safeTransfer(confirmedTimestamps[_inflationRewardTimestampIdx].transmitter, transmitterComission); + IInflationRewardManager(inflationRewardManager).transferInflationRewardToken( + confirmedTimestamps[_inflationRewardTimestampIdx].transmitter, transmitterComission + ); // distribute the remaining inflation reward _distributeInflationReward(_operator, inflationRewardRemaining); + + ISymbioticStakingReward(rewardDistributor).updateInflationReward(_operator, inflationRewardRemaining); } // unlock the stake locked during job creation delete lockInfo[_jobId]; - operatorLockedAmounts[_operator][lock.stakeToken] -= amountToLock[lock.stakeToken]; + operatorLockedAmounts[lock.stakeToken][_operator] -= amountToLock[lock.stakeToken]; // TODO: emit event } @@ -247,7 +280,7 @@ contract SymbioticStaking is uint256 lockedAmount = lock.amount; // unlock the stake locked during job creation - operatorLockedAmounts[_slashedJobs[i].operator][lock.stakeToken] -= lockedAmount; + operatorLockedAmounts[lock.stakeToken][_slashedJobs[i].operator] -= lockedAmount; delete lockInfo[_slashedJobs[i].jobId]; // TODO: emit events? @@ -255,18 +288,24 @@ contract SymbioticStaking is } /// @notice called when pending inflation reward is updated - function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) external onlyStakingManager { - if(_rewardAmount == 0) return; + function distributeInflationReward(address _operator, uint256 _rewardAmount, uint256 _timestampIdx) + external + onlyStakingManager + { + if (_rewardAmount == 0) return; - uint256 transmitterComission = Math.mulDiv(_rewardAmount, confirmedTimestamps[_timestampIdx].transmitterComissionRate, 1e18); + uint256 transmitterComission = + Math.mulDiv(_rewardAmount, confirmedTimestamps[_timestampIdx].transmitterComissionRate, 1e18); uint256 inflationRewardRemaining = _rewardAmount - transmitterComission; // reward the transmitter who created the latestConfirmedTimestamp at the time of job creation IERC20(inflationRewardToken).safeTransfer(confirmedTimestamps[_timestampIdx].transmitter, transmitterComission); uint256 len = stakeTokenSet.length(); - for(uint256 i = 0; i < len; i++) { - _distributeInflationReward(_operator, _calcInflationRewardAmount(stakeTokenSet.at(i), inflationRewardRemaining)); // TODO: gas optimization + for (uint256 i = 0; i < len; i++) { + _distributeInflationReward( + _operator, _calcInflationRewardAmount(stakeTokenSet.at(i), inflationRewardRemaining) + ); // TODO: gas optimization } } @@ -275,7 +314,7 @@ contract SymbioticStaking is /*------------------------------- Snapshot Submission ----------------------------*/ function _checkTransmitterRegistration(uint256 _captureTimestamp) internal { - if(registeredTransmitters[_captureTimestamp] == address(0)) { + if (registeredTransmitters[_captureTimestamp] == address(0)) { // once transmitter is registered, other transmitters cannot submit the snapshot for the same capturetimestamp registeredTransmitters[_captureTimestamp] = msg.sender; } else { @@ -300,10 +339,16 @@ contract SymbioticStaking is Struct.VaultSnapshot memory _vaultSnapshot = _vaultSnapshots[i]; // update vault staked amount - vaultStakeAmounts[_captureTimestamp][_vaultSnapshot.vault][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] = _vaultSnapshot.stakeAmount; + vaultStakeAmounts[_captureTimestamp][_vaultSnapshot.stakeToken][_vaultSnapshot.vault][_vaultSnapshot + .operator] = _vaultSnapshot.stakeAmount; // update operator staked amount - operatorStakeAmounts[_captureTimestamp][_vaultSnapshot.operator][_vaultSnapshot.stakeToken] += _vaultSnapshot.stakeAmount; + operatorStakeAmounts[_captureTimestamp][_vaultSnapshot.stakeToken][_vaultSnapshot.operator] += + _vaultSnapshot.stakeAmount; + + ISymbioticStakingReward(rewardDistributor).onSnapshotSubmission( + _vaultSnapshot.vault, _vaultSnapshot.operator + ); // TODO: emit event for each update? } @@ -312,7 +357,8 @@ contract SymbioticStaking is function _completeSubmission(uint256 _captureTimestamp) internal { uint256 transmitterComission = _calcTransmitterComissionRate(_captureTimestamp); - Struct.ConfirmedTimestamp memory confirmedTimestamp = Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); + Struct.ConfirmedTimestamp memory confirmedTimestamp = + Struct.ConfirmedTimestamp(_captureTimestamp, msg.sender, transmitterComission); confirmedTimestamps.push(confirmedTimestamp); IInflationRewardManager(inflationRewardManager).updateEpochTimestampIdx(); @@ -321,10 +367,7 @@ contract SymbioticStaking is /*------------------------------ Reward Distribution -----------------------------*/ - function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal { - ISymbioticStakingReward(rewardDistributor).updateFeeReward(_stakeToken, _operator, _amount); - } - + function _distributeFeeReward(address _stakeToken, address _operator, uint256 _amount) internal {} function _distributeInflationReward(address _operator, uint256 _amount) internal { ISymbioticStakingReward(rewardDistributor).updateInflationReward(_operator, _amount); @@ -333,50 +376,78 @@ contract SymbioticStaking is /*================================================== external view ==================================================*/ function latestConfirmedTimestamp() public view returns (uint256) { - return confirmedTimestamps[latestConfirmedTimestampIdx()].captureTimestamp; + uint256 len = confirmedTimestamps.length; + return len > 0 ? confirmedTimestamps[len - 1].captureTimestamp : 0; + } + + function confirmedTimestampInfo(uint256 _idx) public view returns (Struct.ConfirmedTimestamp memory) { + return confirmedTimestamps[_idx]; } function latestConfirmedTimestampIdx() public view returns (uint256) { - return confirmedTimestamps.length - 1; + uint256 len = confirmedTimestamps.length; + return len > 0 ? len - 1 : 0; } - function getOperatorStakeAmount(address _operator, address _token) public view returns (uint256) { - return operatorStakeAmounts[latestConfirmedTimestamp()][_operator][_token]; + function getOperatorStakeAmount(address _stakeToken, address _operator) public view returns (uint256) { + return operatorStakeAmounts[latestConfirmedTimestamp()][_stakeToken][_operator]; } - function getOperatorActiveStakeAmount(address _operator, address _token) public view returns (uint256) { - uint256 operatorStakeAmount = getOperatorStakeAmount(_operator, _token); - uint256 operatorLockedAmount = operatorLockedAmounts[_operator][_token]; + function getOperatorActiveStakeAmount(address _stakeToken, address _operator) public view returns (uint256) { + uint256 operatorStakeAmount = getOperatorStakeAmount(_stakeToken, _operator); + uint256 operatorLockedAmount = operatorLockedAmounts[_stakeToken][_operator]; return operatorStakeAmount > operatorLockedAmount ? operatorStakeAmount - operatorLockedAmount : 0; } - function getStakeAmount(address _vault, address _stakeToken, address _operator) external view returns (uint256) { - return vaultStakeAmounts[latestConfirmedTimestamp()][_vault][_stakeToken][_operator]; + function getStakeAmount(address _stakeToken, address _vault, address _operator) external view returns (uint256) { + return vaultStakeAmounts[latestConfirmedTimestamp()][_stakeToken][_vault][_operator]; } - function getStakeTokenList() external view returns(address[] memory) { + function getStakeTokenList() external view returns (address[] memory) { return stakeTokenSet.values(); } - function isSupportedStakeToken(address _token) public view returns (bool) { - return stakeTokenSet.contains(_token); + function getStakeTokenWeights() external view returns (address[] memory, uint256[] memory) { + uint256[] memory weights = new uint256[](stakeTokenSet.length()); + for (uint256 i = 0; i < stakeTokenSet.length(); i++) { + weights[i] = tokenSelectionWeight[stakeTokenSet.at(i)]; + } + return (stakeTokenSet.values(), weights); + } + + function isSupportedStakeToken(address _stakeToken) public view returns (bool) { + return stakeTokenSet.contains(_stakeToken); + } + + function getTxCountInfo(uint256 _captureTimestamp, address _transmitter, bytes32 _type) + external + view + returns (Struct.SnapshotTxCountInfo memory) + { + return txCountInfo[_captureTimestamp][_transmitter][_type]; + } + + function getSubmissionStatus(uint256 _captureTimestamp, address _transmitter) external view returns (bytes32) { + return submissionStatus[_captureTimestamp][_transmitter]; } /*================================================== internal view ==================================================*/ /*------------------------------ Snapshot Submission -----------------------------*/ - function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) internal view { + function _checkValidity(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes32 _type) + internal + view + { require(_numOfTxs > 0, "Invalid length"); // snapshot cannot be submitted before the cooldown period from the last confirmed timestamp (completed snapshot submission) - require(block.timestamp >= (latestConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); + require(_captureTimestamp >= (latestConfirmedTimestamp() + submissionCooldown), "Cooldown period not passed"); + require(_captureTimestamp <= block.timestamp, "Invalid timestamp"); - Struct.SnapshotTxCountInfo memory snapshot = txCountInfo[_captureTimestamp][msg.sender][_type]; require(_index == snapshot.idxToSubmit, "Invalid index"); - require(_index < snapshot.numOfTxs, "Invalid index"); - require(snapshot.numOfTxs == _numOfTxs, "Invalid numOfTxs"); + require(_index < _numOfTxs, "Invalid index"); // here we assume enclave submis the correct data bytes32 mask; if (_type == STAKE_SNAPSHOT_TYPE) mask = STAKE_SNAPSHOT_MASK; @@ -385,7 +456,13 @@ contract SymbioticStaking is require(submissionStatus[_captureTimestamp][msg.sender] & mask == 0, "Already submitted"); } - function _verifySignature(uint256 _index, uint256 _numOfTxs, uint256 _captureTimestamp, bytes memory _data, bytes memory _signature) internal { + function _verifySignature( + uint256 _index, + uint256 _numOfTxs, + uint256 _captureTimestamp, + bytes memory _data, + bytes memory _signature + ) internal { // TODO: Verify the signature // TODO: "signature" should be from the enclave key that is verified against the PCR values of the bridge enclave image } @@ -394,8 +471,9 @@ contract SymbioticStaking is return submissionStatus[_captureTimestamp][msg.sender] == COMPLETE_MASK; } - function _calcTransmitterComissionRate(uint256 _confirmedTimestamp) internal view returns(uint256) { + function _calcTransmitterComissionRate(uint256 _confirmedTimestamp) internal view returns (uint256) { // TODO: (block.timestamp - _lastConfirmedTimestamp) * X + return baseTransmitterComissionRate; } function _currentTransmitter() internal view returns (address) { @@ -404,7 +482,7 @@ contract SymbioticStaking is /*-------------------------------------- Job -------------------------------------*/ - function _selectStakeToken(address _operator) internal view returns(address) { + function _selectStakeToken(address _operator) internal view returns (address) { require(tokenSelectionWeightSum > 0, "Total weight must be greater than zero"); require(stakeTokenSet.length() > 0, "No tokens available"); @@ -431,11 +509,12 @@ contract SymbioticStaking is require(idx > 0, "No stakeToken available"); // random number in range [0, weightSum - 1] - uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1), msg.sender))) % weightSum; + uint256 random = uint256( + keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1), msg.sender)) + ) % weightSum; uint256 cumulativeWeight = 0; address selectedToken; - uint256 i; // select token based on weight @@ -448,21 +527,21 @@ contract SymbioticStaking is } // check if the selected token has enough active stake amount - if (getOperatorActiveStakeAmount(_operator, selectedToken) >= amountToLock[selectedToken]) { + if (getOperatorActiveStakeAmount(selectedToken, _operator) >= amountToLock[selectedToken]) { return selectedToken; } weightSum -= weights[i]; tokens[i] = tokens[idx - 1]; weights[i] = weights[idx - 1]; - idx--; // 배열 크기를 줄임 + idx--; // 배열 크기를 줄임 } // this should be returned - return address(0); + return address(0); } - function _getActiveStakeAmount(address _stakeToken) internal view returns(uint256) { + function _getActiveStakeAmount(address _stakeToken) internal view returns (uint256) { // TODO } @@ -473,7 +552,11 @@ contract SymbioticStaking is /*------------------------------------ Reward ------------------------------------*/ - function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) internal view returns(uint256) { + function _calcInflationRewardAmount(address _stakeToken, uint256 _inflationRewardAmount) + internal + view + returns (uint256) + { return Math.mulDiv(_inflationRewardAmount, inflationRewardShare[_stakeToken], 1e18); } @@ -485,23 +568,27 @@ contract SymbioticStaking is // TODO: emit event } - function addStakeToken(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(stakeTokenSet.add(_token), "Token already exists"); - + function addStakeToken(address _stakeToken, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.add(_stakeToken), "Token already exists"); + tokenSelectionWeightSum += _weight; - inflationRewardShare[_token] = _weight; + tokenSelectionWeight[_stakeToken] = _weight; + } + + function setAmountToLock(address _stakeToken, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + amountToLock[_stakeToken] = _amount; } - function removeStakeToken(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(stakeTokenSet.remove(_token), "Token does not exist"); - - tokenSelectionWeightSum -= inflationRewardShare[_token]; - delete inflationRewardShare[_token]; + function removeStakeToken(address _stakeToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(stakeTokenSet.remove(_stakeToken), "Token does not exist"); + + tokenSelectionWeightSum -= tokenSelectionWeight[_stakeToken]; + delete tokenSelectionWeight[_stakeToken]; } - function setStakeTokenWeight(address _token, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { - tokenSelectionWeightSum -= tokenSelectionWeight[_token]; - tokenSelectionWeight[_token] = _weight; + function setStakeTokenWeight(address _stakeToken, uint256 _weight) external onlyRole(DEFAULT_ADMIN_ROLE) { + tokenSelectionWeightSum -= tokenSelectionWeight[_stakeToken]; + tokenSelectionWeight[_stakeToken] = _weight; tokenSelectionWeightSum += _weight; } @@ -510,7 +597,7 @@ contract SymbioticStaking is // TODO: emit event } - + /// @dev base transmitter comission rate is in range [0, 1e18) function setBaseTransmitterComissionRate(uint256 _baseTransmitterComission) external onlyRole(DEFAULT_ADMIN_ROLE) { require(_baseTransmitterComission < 1e18, "Invalid comission rate"); diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 05551af..2f5e45f 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -10,6 +10,7 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/Pau import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {JobManager} from "./JobManager.sol"; import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; @@ -46,19 +47,19 @@ contract SymbioticStakingReward is uint256[500] private __gap_1; // rewardTokens amount per stakeToken - mapping(address stakeToken => mapping(address operator => mapping(address rewardToken => uint256 rewardPerToken))) + mapping(address stakeToken => mapping(address rewardToken => mapping(address operator => uint256 rewardPerToken))) public rewardPerTokenStored; mapping( - address vault + address stakeToken => mapping( - address stakeToken - => mapping(address operator => mapping(address rewardToken => uint256 rewardPerTokenPaid)) + address rewardToken + => mapping(address vault => mapping(address operator => uint256 rewardPerTokenPaid)) ) - ) rewardPerTokenPaids; + ) public rewardPerTokenPaids; // reward accrued that the vault can claim - mapping(address vault => mapping(address rewardToken => uint256 amount)) public rewardAccrued; + mapping(address rewardToken => mapping(address vault => uint256 amount)) public rewardAccrued; modifier onlySymbioticStaking() { @@ -111,8 +112,12 @@ contract SymbioticStakingReward is external onlySymbioticStaking { - rewardPerTokenStored[_stakeToken][_operator][feeRewardToken] += - _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, _stakeToken)); + uint256 operatorStakeAmount = _getOperatorStakeAmount(_stakeToken, _operator); + if (operatorStakeAmount > 0) { + rewardPerTokenStored[_stakeToken][feeRewardToken][_operator] += + _rewardAmount.mulDiv(1e18, operatorStakeAmount); + } + } /// @notice called when inflation reward is generated @@ -120,11 +125,19 @@ contract SymbioticStakingReward is function updateInflationReward(address _operator, uint256 _rewardAmount) external onlySymbioticStaking { address[] memory stakeTokenList = _getStakeTokenList(); for (uint256 i = 0; i < stakeTokenList.length; i++) { - rewardPerTokenStored[stakeTokenList[i]][_operator][inflationRewardToken] += - _rewardAmount.mulDiv(1e18, _getOperatorStakeAmount(_operator, stakeTokenList[i])); + uint256 operatorStakeAmount = _getOperatorStakeAmount(stakeTokenList[i], _operator); + // TODO: fix this logic + if (operatorStakeAmount > 0) { + rewardPerTokenStored[stakeTokenList[i]][inflationRewardToken][_operator] += + _rewardAmount.mulDiv(1e18, operatorStakeAmount); + } } } + function onSnapshotSubmission(address _vault, address _operator) external onlySymbioticStaking { + _updateVaultReward(_getStakeTokenList(), _vault, _operator); + } + /* ------------------------- reward claim ------------------------- */ /// @notice vault can claim reward calling this function @@ -133,23 +146,34 @@ contract SymbioticStakingReward is _updatePendingInflaionReward(_operator); // update rewardPerTokenPaid and rewardAccrued for each vault - _updateVaultInflationReward(_getStakeTokenList(), _msgSender(), _operator); + _updateVaultReward(_getStakeTokenList(), _msgSender(), _operator); + + address[] memory stakeTokenList = _getStakeTokenList(); + for (uint256 i = 0; i < stakeTokenList.length; i++) { + } + // TODO: check transfer logic // transfer fee reward to the vault - IERC20(feeRewardToken).safeTransferFrom(jobManager, _msgSender(), rewardAccrued[_msgSender()][feeRewardToken]); - rewardAccrued[_msgSender()][feeRewardToken] = 0; + uint256 feeRewardAmount = rewardAccrued[_msgSender()][feeRewardToken]; + if (feeRewardAmount > 0) { + JobManager(jobManager).transferFeeToken(_msgSender(), feeRewardAmount); + rewardAccrued[_msgSender()][feeRewardToken] = 0; + } // transfer inflation reward to the vault - IERC20(inflationRewardToken).safeTransferFrom(jobManager, _msgSender(), rewardAccrued[_msgSender()][inflationRewardToken]); - rewardAccrued[_msgSender()][inflationRewardToken] = 0; + uint256 inflationRewardAmount = rewardAccrued[_msgSender()][inflationRewardToken]; + if (inflationRewardAmount > 0) { + IInflationRewardManager(inflationRewardManager).transferInflationRewardToken(_msgSender(), inflationRewardAmount); + rewardAccrued[_msgSender()][inflationRewardToken] = 0; + } } /*================================================== external view ==================================================*/ function getVaultRewardAccrued(address _vault) external view returns (uint256 feeReward, uint256 inflationReward) { // TODO: this does not include pending inflation reward as it requires states update in JobManager - return (rewardAccrued[_vault][feeRewardToken], rewardAccrued[_vault][inflationRewardToken]); + return (rewardAccrued[feeRewardToken][_vault], rewardAccrued[inflationRewardToken][_vault]); } /*===================================================== internal ====================================================*/ @@ -160,21 +184,37 @@ contract SymbioticStakingReward is } /// @dev update rewardPerToken and rewardAccrued for each vault - function _updateVaultInflationReward(address[] memory _stakeTokenList, address _vault, address _operator) + function _updateVaultReward(address[] memory _stakeTokenList, address _vault, address _operator) internal - { + { for (uint256 i = 0; i < _stakeTokenList.length; i++) { address stakeToken = _stakeTokenList[i]; - uint256 operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][_operator][inflationRewardToken]; - uint256 vaultRewardPerTokenPaid = rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken]; + + /* fee reward */ + uint256 operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][feeRewardToken][_operator]; + uint256 vaultRewardPerTokenPaid = rewardPerTokenPaids[stakeToken][feeRewardToken][_vault][_operator]; + + // update reward accrued for the vault + rewardAccrued[_vault][feeRewardToken] += _getVaultStakeAmount(stakeToken, _vault, _operator).mulDiv( + operatorRewardPerTokenStored - vaultRewardPerTokenPaid, 1e18 + ); + + + // update rewardPerTokenPaid of the vault + rewardPerTokenPaids[stakeToken][feeRewardToken][_vault][_operator] = operatorRewardPerTokenStored; + + + /* inflation reward */ + operatorRewardPerTokenStored = rewardPerTokenStored[stakeToken][inflationRewardToken][_operator]; + vaultRewardPerTokenPaid = rewardPerTokenPaids[stakeToken][inflationRewardToken][_vault][_operator]; // update reward accrued for the vault - rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(_vault, stakeToken, _operator).mulDiv( + rewardAccrued[_vault][inflationRewardToken] += _getVaultStakeAmount(stakeToken, _vault, _operator).mulDiv( operatorRewardPerTokenStored - vaultRewardPerTokenPaid, 1e18 ); // update rewardPerTokenPaid of the vault - rewardPerTokenPaids[_vault][stakeToken][_operator][inflationRewardToken] = operatorRewardPerTokenStored; + rewardPerTokenPaids[stakeToken][inflationRewardToken][_vault][_operator] = operatorRewardPerTokenStored; } } @@ -184,12 +224,12 @@ contract SymbioticStakingReward is return ISymbioticStaking(symbioticStaking).getStakeTokenList(); } - function _getOperatorStakeAmount(address _operator, address _stakeToken) internal view returns (uint256) { - return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_operator, _stakeToken); + function _getOperatorStakeAmount(address _stakeToken, address _operator) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getOperatorStakeAmount(_stakeToken, _operator); } - function _getVaultStakeAmount(address _vault, address _stakeToken, address _operator) internal view returns (uint256) { - return ISymbioticStaking(symbioticStaking).getStakeAmount(_vault, _stakeToken, _operator); + function _getVaultStakeAmount(address _stakeToken, address _vault, address _operator) internal view returns (uint256) { + return ISymbioticStaking(symbioticStaking).getStakeAmount(_stakeToken, _vault,_operator); } /*======================================================= admin =====================================================*/ From 95f7fcb30830822a15af97bfbcb6e209f761bcf9 Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 14 Oct 2024 14:10:32 +0900 Subject: [PATCH 157/158] e2e test --- .../l2_contracts/InflationRewardManger.sol | 2 - test/foundry/TestSetup.t.sol | 123 ++++++-- test/foundry/e2e/KalypsoStaking.t.sol | 298 +++++++++++++++++- 3 files changed, 376 insertions(+), 47 deletions(-) diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 2374b3c..591052e 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -16,8 +16,6 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {console} from "forge-std/Test.sol"; - contract InflationRewardManager is ContextUpgradeable, ERC165Upgradeable, diff --git a/test/foundry/TestSetup.t.sol b/test/foundry/TestSetup.t.sol index 6c4af4d..f8acf8d 100644 --- a/test/foundry/TestSetup.t.sol +++ b/test/foundry/TestSetup.t.sol @@ -32,20 +32,22 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract TestSetup is Test { - uint256 constant TWENTY_PERCENT = 20; - uint256 constant THIRTY_PERCENT = 30; - uint256 constant FORTY_PERCENT = 40; - uint256 constant FIFTY_PERCENT = 50; - uint256 constant SIXTY_PERCENT = 60; - uint256 constant HUNDRED_PERCENT = 100; - - uint256 constant FUND_FOR_GAS = 10 ether; // 10 ether - uint256 constant FUND_FOR_FEE = 10_000 ether; // 10,000 USDC - uint256 constant FUND_FOR_SELF_STAKE = 1000_000 ether; // 10,000 POND - uint256 constant FUND_FOR_INFLATION_REWARD = 100_000 ether; // 100,000 POND + uint256 constant public TWENTY_PERCENT = 20; + uint256 constant public THIRTY_PERCENT = 30; + uint256 constant public FORTY_PERCENT = 40; + uint256 constant public FIFTY_PERCENT = 50; + uint256 constant public SIXTY_PERCENT = 60; + uint256 constant public HUNDRED_PERCENT = 100; + + uint256 constant public FUND_FOR_GAS = 10 ether; // 10 ether + uint256 constant public FUND_FOR_FEE = 10_000 ether; // 10,000 USDC + uint256 constant public FUND_FOR_SELF_STAKE = 1000_000 ether; // 10,000 POND + uint256 constant public FUND_FOR_INFLATION_REWARD = 100_000 ether; // 100,000 POND - uint256 constant INFLATION_REWARD_EPOCH_SIZE = 30 minutes; // 30 minutes - uint256 constant INFLATION_REWARD_PER_EPOCH = 1000 ether; // 1,000 POND + uint256 constant public INFLATION_REWARD_EPOCH_SIZE = 30 minutes; // 30 minutes + uint256 constant public INFLATION_REWARD_PER_EPOCH = 100 ether; // 1,000 POND + + uint256 constant public SUBMISSION_COOLDOWN = 12 hours; /* contracts */ @@ -69,18 +71,31 @@ contract TestSetup is Test { /* admin */ address public deployer; address public admin; - address public vault; // holds inflation reward tokens + address public inflationRewardVault; // holds inflation reward tokens /* operators */ address public operatorA; address public operatorB; address public operatorC; + /* symbiotic vaults */ + address public symbioticVaultA; + address public symbioticVaultB; + address public symbioticVaultC; + + /* transmitters */ + address public transmitterA; + address public transmitterB; + address public transmitterC; + /* stakers */ address public stakerA; address public stakerB; address public stakerC; + /* slasher */ + address public slasher; + /* job requesters */ address public jobRequesterA; address public jobRequesterB; @@ -91,7 +106,9 @@ contract TestSetup is Test { /* set address */ deployer = makeAddr("deployer"); admin = makeAddr("admin"); - vault = makeAddr("vault"); + inflationRewardVault = makeAddr("inflationRewardVault"); + + slasher = makeAddr("slasher"); stakerA = makeAddr("stakerA"); stakerB = makeAddr("stakerB"); @@ -101,6 +118,14 @@ contract TestSetup is Test { operatorB = makeAddr("operatorB"); operatorC = makeAddr("operatorC"); + symbioticVaultA = makeAddr("symbioticVaultA"); + symbioticVaultB = makeAddr("symbioticVaultB"); + symbioticVaultC = makeAddr("symbioticVaultC"); + + transmitterA = makeAddr("transmitterA"); + transmitterB = makeAddr("transmitterB"); + transmitterC = makeAddr("transmitterC"); + jobRequesterA = makeAddr("jobRequesterA"); jobRequesterB = makeAddr("jobRequesterB"); jobRequesterC = makeAddr("jobRequesterC"); @@ -108,16 +133,21 @@ contract TestSetup is Test { /* fund gas */ vm.deal(deployer, FUND_FOR_GAS); vm.deal(admin, FUND_FOR_GAS); - vm.deal(vault, FUND_FOR_GAS); + vm.deal(inflationRewardVault, FUND_FOR_GAS); vm.deal(operatorA, FUND_FOR_GAS); vm.deal(operatorB, FUND_FOR_GAS); vm.deal(operatorC, FUND_FOR_GAS); + vm.deal(slasher, FUND_FOR_GAS); vm.deal(stakerA, FUND_FOR_GAS); vm.deal(stakerB, FUND_FOR_GAS); vm.deal(stakerC, FUND_FOR_GAS); + vm.deal(transmitterA, FUND_FOR_GAS); + vm.deal(transmitterB, FUND_FOR_GAS); + vm.deal(transmitterC, FUND_FOR_GAS); + vm.deal(jobRequesterA, FUND_FOR_GAS); vm.deal(jobRequesterB, FUND_FOR_GAS); vm.deal(jobRequesterC, FUND_FOR_GAS); @@ -125,6 +155,7 @@ contract TestSetup is Test { /* label */ vm.label(deployer, "deployer"); vm.label(admin, "admin"); + vm.label(slasher, "slasher"); vm.label(operatorA, "operatorA"); vm.label(operatorB, "operatorB"); @@ -134,9 +165,17 @@ contract TestSetup is Test { vm.label(stakerB, "stakerB"); vm.label(stakerC, "stakerC"); + vm.label(symbioticVaultA, "symbioticVaultA"); + vm.label(symbioticVaultB, "symbioticVaultB"); + vm.label(symbioticVaultC, "symbioticVaultC"); + vm.label(jobRequesterA, "jobRequesterA"); vm.label(jobRequesterB, "jobRequesterB"); vm.label(jobRequesterC, "jobRequesterC"); + + vm.label(transmitterA, "transmitterA"); + vm.label(transmitterB, "transmitterB"); + vm.label(transmitterC, "transmitterC"); } /*======================================== internal ========================================*/ @@ -150,10 +189,12 @@ contract TestSetup is Test { vm.startPrank(deployer); // FeeToken - feeToken = address(new USDC(admin)); + usdc = address(new USDC(admin)); + feeToken = usdc; // InflationRewardToken - inflationRewardToken = address(new POND(admin)); + pond = address(new POND(admin)); + inflationRewardToken = pond; // stakeToken weth = address(new WETH(admin)); @@ -190,7 +231,7 @@ contract TestSetup is Test { // JobManager JobManager(address(jobManager)).initialize( - admin, address(stakingManager), address(feeToken), address(inflationRewardManager), 1 hours + admin, address(stakingManager), address(symbioticStaking), address(symbioticStakingReward), address(feeToken), address(inflationRewardManager), 1 hours ); assertEq(JobManager(jobManager).hasRole(JobManager(jobManager).DEFAULT_ADMIN_ROLE(), admin), true); @@ -198,6 +239,7 @@ contract TestSetup is Test { StakingManager(address(stakingManager)).initialize( admin, address(jobManager), + address(symbioticStaking), address(inflationRewardManager), address(feeToken), address(inflationRewardToken) @@ -218,21 +260,22 @@ contract TestSetup is Test { // SymbioticStaking SymbioticStaking(address(symbioticStaking)).initialize( admin, - address(stakingManager), - address(symbioticStakingReward), - address(inflationRewardManager), - address(feeToken), - address(inflationRewardToken) + jobManager, + stakingManager, + symbioticStakingReward, + inflationRewardManager, + feeToken, + inflationRewardToken ); assertEq(SymbioticStaking(symbioticStaking).hasRole(SymbioticStaking(symbioticStaking).DEFAULT_ADMIN_ROLE(), admin), true); // SymbioticStakingReward SymbioticStakingReward(address(symbioticStakingReward)).initialize( admin, - address(inflationRewardManager), - address(jobManager), - address(symbioticStaking), - address(feeToken), - address(inflationRewardToken) + inflationRewardManager, + jobManager, + symbioticStaking, + feeToken, + inflationRewardToken ); assertEq(SymbioticStakingReward(symbioticStakingReward).hasRole(SymbioticStakingReward(symbioticStakingReward).DEFAULT_ADMIN_ROLE(), admin), true); @@ -240,9 +283,11 @@ contract TestSetup is Test { InflationRewardManager(address(inflationRewardManager)).initialize( admin, block.timestamp, - address(jobManager), - address(stakingManager), - address(inflationRewardToken), + jobManager, + stakingManager, + symbioticStaking, + symbioticStakingReward, + inflationRewardToken, INFLATION_REWARD_EPOCH_SIZE, // inflationRewardEpochSize INFLATION_REWARD_PER_EPOCH // inflationRewardPerEpoch ); @@ -285,6 +330,7 @@ contract TestSetup is Test { function _setNativeStakingConfig() internal { vm.startPrank(admin); NativeStaking(nativeStaking).addStakeToken(pond, _calcShareAmount(HUNDRED_PERCENT)); + NativeStaking(nativeStaking).setAmountToLock(pond, 1 ether); vm.stopPrank(); } @@ -298,16 +344,27 @@ contract TestSetup is Test { /* base transmitter comission rate and submission cooldown */ SymbioticStaking(symbioticStaking).setBaseTransmitterComissionRate(_calcShareAmount(TWENTY_PERCENT)); SymbioticStaking(symbioticStaking).setSubmissionCooldown(12 hours); + + /* amount to lock */ + SymbioticStaking(symbioticStaking).setAmountToLock(pond, 0.2 ether); + SymbioticStaking(symbioticStaking).setAmountToLock(weth, 0.2 ether); + vm.stopPrank(); assertEq(SymbioticStaking(symbioticStaking).baseTransmitterComissionRate(), _calcShareAmount(TWENTY_PERCENT)); - assertEq(SymbioticStaking(symbioticStaking).submissionCooldown(), 12 hours); + assertEq(SymbioticStaking(symbioticStaking).submissionCooldown(), SUBMISSION_COOLDOWN); } function _fund_tokens() internal { deal(pond, operatorA, FUND_FOR_SELF_STAKE); deal(pond, operatorB, FUND_FOR_SELF_STAKE); deal(pond, operatorC, FUND_FOR_SELF_STAKE); + + deal(usdc, jobRequesterA, FUND_FOR_FEE); + deal(usdc, jobRequesterB, FUND_FOR_FEE); + deal(usdc, jobRequesterC, FUND_FOR_FEE); + + deal(inflationRewardToken, inflationRewardManager, FUND_FOR_INFLATION_REWARD); } diff --git a/test/foundry/e2e/KalypsoStaking.t.sol b/test/foundry/e2e/KalypsoStaking.t.sol index 14ea5c8..9d67fb9 100644 --- a/test/foundry/e2e/KalypsoStaking.t.sol +++ b/test/foundry/e2e/KalypsoStaking.t.sol @@ -9,13 +9,14 @@ import {TestSetup} from "../TestSetup.t.sol"; import {JobManager} from "../../../contracts/staking/l2_contracts/JobManager.sol"; import {StakingManager} from "../../../contracts/staking/l2_contracts/StakingManager.sol"; import {SymbioticStaking} from "../../../contracts/staking/l2_contracts/SymbioticStaking.sol"; +import {SymbioticStakingReward} from "../../../contracts/staking/l2_contracts/SymbioticStakingReward.sol"; /* interfaces */ import {IJobManager} from "../../../contracts/interfaces/staking/IJobManager.sol"; import {IStakingManager} from "../../../contracts/interfaces/staking/IStakingManager.sol"; import {INativeStaking} from "../../../contracts/interfaces/staking/INativeStaking.sol"; import {ISymbioticStaking} from "../../../contracts/interfaces/staking/ISymbioticStaking.sol"; - +import {ISymbioticStakingReward} from "../../../contracts/interfaces/staking/ISymbioticStakingReward.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /* libraries */ @@ -26,6 +27,10 @@ contract KalypsoStakingTest is Test, TestSetup { uint256 constant OPERATORA_SELF_STAKE_AMOUNT = 1000 ether; uint256 constant OPERATORB_SELF_STAKE_AMOUNT = 2000 ether; + uint256 constant VAULT_A_INTO_OPERATOR_A = 1000 ether; + uint256 constant VAULT_B_INTO_OPERATOR_A = 2000 ether; + uint256 constant VAULT_B_INTO_OPERATOR_B = 3000 ether; + function setUp() public { _setupAddr(); _setupContracts(); @@ -43,18 +48,50 @@ contract KalypsoStakingTest is Test, TestSetup { /* SymbioticStaking */ _setSymbioticStakingConfig(); - } /// @notice test full lifecycle of kalypso staking function test_kalypso_staking() public { - /*-------------------- Native Staking Stake --------------------*/ + /* current block nubmer: 50_001 */ + vm.warp(block.timestamp + 50_000); + assertEq(block.timestamp, 50_001); + // operators self stake _operator_self_stake(); - // _symbiotic_staking_snapshot_transmission(); + // symbiotic staking snapshot submitted + _symbiotic_staking_snapshot_submission(); + + // jobId1 created + _create_job_1(); + + vm.warp(block.timestamp + 10 minutes); + + // proof submitted + _submit_proof_job_1(); + + // symbioticVaultA claims fee reward (no inflation reward from job1 generated yet) + _vault_claim_reward_from_job_1(); + + // jobId2 created + vm.warp(block.timestamp + INFLATION_REWARD_EPOCH_SIZE); // inflation reward for job1 should be generated + _create_job_2(); + + // jobId2 completed + _submit_proof_job_2(); // TODO: inflation reward generated + + // fee reward and inflation reward distributed + + // job created + _create_job_3(); + + // job slashed in Symbiotic Staking and result submitted + vm.warp(block.timestamp + SUBMISSION_COOLDOWN); + _slash_result_submission_job_3(); } + /*===================================================== internal ====================================================*/ + function _operator_self_stake() internal { // Operator A self stakes into 1_000 POND vm.startPrank(operatorA); @@ -64,32 +101,269 @@ contract KalypsoStakingTest is Test, TestSetup { // weth is not supported in NativeStaking vm.expectRevert("Token not supported"); - INativeStaking(nativeStaking).stake(operatorA, weth, OPERATORA_SELF_STAKE_AMOUNT); + INativeStaking(nativeStaking).stake(weth, operatorA, OPERATORA_SELF_STAKE_AMOUNT); // only operator can stake vm.expectRevert("Only operator can stake"); - INativeStaking(nativeStaking).stake(operatorB, pond, OPERATORA_SELF_STAKE_AMOUNT); + INativeStaking(nativeStaking).stake(pond, operatorB, OPERATORA_SELF_STAKE_AMOUNT); // stake 1000 POND - INativeStaking(nativeStaking).stake(operatorA, pond, OPERATORA_SELF_STAKE_AMOUNT); + INativeStaking(nativeStaking).stake(pond, operatorA, OPERATORA_SELF_STAKE_AMOUNT); } vm.stopPrank(); - assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(operatorA, pond), OPERATORA_SELF_STAKE_AMOUNT); - assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(operatorA, pond), OPERATORA_SELF_STAKE_AMOUNT); + assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(pond, operatorA), OPERATORA_SELF_STAKE_AMOUNT); + assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(pond, operatorA), OPERATORA_SELF_STAKE_AMOUNT); vm.startPrank(operatorB); { IERC20(pond).approve(nativeStaking, type(uint256).max); - INativeStaking(nativeStaking).stake(operatorB, pond, OPERATORB_SELF_STAKE_AMOUNT); + INativeStaking(nativeStaking).stake(pond, operatorB, OPERATORB_SELF_STAKE_AMOUNT); } vm.stopPrank(); - assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(operatorB, pond), OPERATORB_SELF_STAKE_AMOUNT); - assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(operatorB, pond), OPERATORB_SELF_STAKE_AMOUNT); + assertEq(INativeStaking(nativeStaking).getOperatorStakeAmount(pond, operatorB), OPERATORB_SELF_STAKE_AMOUNT, "Stake amount mismatch"); + assertEq(INativeStaking(nativeStaking).getOperatorActiveStakeAmount(pond, operatorB), OPERATORB_SELF_STAKE_AMOUNT, "Active stake amount mismatch"); } function _symbiotic_staking_snapshot_submission() internal { + /* + < TransmitterA Transmits > + OperatorA: opted-into symbioticVaultA (weth) - 1000 weth, + OperatorB: opted-into symbioticVaultA (weth) - 2000 weth, symbioticVaultB (pond) - 3000 pond + */ + + // Partial Tx 1 + Struct.VaultSnapshot[] memory _vaultSnapshots1 = new Struct.VaultSnapshot[](1); + /* Vault A */ + // VaultA(1000 WETH) -> OperatorA + _vaultSnapshots1[0].operator = operatorA; + _vaultSnapshots1[0].vault = symbioticVaultA; + _vaultSnapshots1[0].stakeToken = weth; + _vaultSnapshots1[0].stakeAmount = VAULT_A_INTO_OPERATOR_A; + + // Partial Tx 2 + Struct.VaultSnapshot[] memory _vaultSnapshots2 = new Struct.VaultSnapshot[](2); + + /* Vault B */ + + // VaultA(2000 weth) -> OperatorB + _vaultSnapshots2[0].operator = operatorB; + _vaultSnapshots2[0].vault = symbioticVaultA; + _vaultSnapshots2[0].stakeToken = weth; + _vaultSnapshots2[0].stakeAmount = VAULT_B_INTO_OPERATOR_A; + + // VaultB(3000 POND) -> OperatorB + _vaultSnapshots2[1].operator = operatorB; + _vaultSnapshots2[1].vault = symbioticVaultB; + _vaultSnapshots2[1].stakeToken = pond; + _vaultSnapshots2[1].stakeAmount = VAULT_B_INTO_OPERATOR_B; + + + /* Snapshot Submission */ + vm.startPrank(transmitterA); + { + vm.expectRevert("Invalid index"); + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(3, 2, abi.encode(block.timestamp - 5, _vaultSnapshots1), ""); + + vm.expectRevert("Invalid index"); + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(2, 2, abi.encode(block.timestamp - 5, _vaultSnapshots1), ""); + + vm.expectRevert("Invalid timestamp"); + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(1, 2, abi.encode(block.timestamp + 1, _vaultSnapshots1), ""); + + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(0, 2, abi.encode(block.timestamp - 5, _vaultSnapshots1), ""); + } + vm.stopPrank(); + Struct.SnapshotTxCountInfo memory _txCountInfo = ISymbioticStaking(symbioticStaking).getTxCountInfo(block.timestamp - 5, transmitterA, keccak256("STAKE_SNAPSHOT_TYPE")); + + assertEq(_txCountInfo.idxToSubmit, 1); + assertEq(_txCountInfo.numOfTxs, 2); + assertEq(ISymbioticStaking(symbioticStaking).getSubmissionStatus(block.timestamp - 5, transmitterA), 0x0, "Submission status mismatch"); + + vm.startPrank(transmitterA); + { + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(1, 2, abi.encode(block.timestamp - 5, _vaultSnapshots2), ""); + } + vm.stopPrank(); + + _txCountInfo = ISymbioticStaking(symbioticStaking).getTxCountInfo(block.timestamp - 5, transmitterA, keccak256("STAKE_SNAPSHOT_TYPE")); + assertEq(_txCountInfo.idxToSubmit, 2); + assertEq(_txCountInfo.numOfTxs, 2); + assertEq(ISymbioticStaking(symbioticStaking).getSubmissionStatus(block.timestamp - 5, transmitterA), 0x0000000000000000000000000000000000000000000000000000000000000001, "Submission status mismatch"); + + /* Slash Result Submission */ + vm.prank(transmitterA); + ISymbioticStaking(symbioticStaking).submitSlashResult(0, 1, abi.encode(block.timestamp - 5, ""), ""); + } + + function _create_job_1() internal { + // requesterA creates a job + vm.startPrank(jobRequesterA); + { + IERC20(feeToken).approve(jobManager, type(uint256).max); + uint256 jobmanagerBalanceBefore = IERC20(feeToken).balanceOf(jobManager); + + vm.expectRevert("No stakeToken available"); + IJobManager(jobManager).createJob(1, jobRequesterA, operatorC, 1 ether); // should revert as operatorC didn't stake any token to NativeStaking + + // pay 1 usdc as fee + IJobManager(jobManager).createJob(1, jobRequesterA, operatorA, 1 ether); + assertEq(IERC20(feeToken).balanceOf(jobManager) - jobmanagerBalanceBefore, 1 ether); + } + vm.stopPrank(); + } + + function _submit_proof_job_1() internal { + Struct.ConfirmedTimestamp memory _confirmedTimestampInfo = ISymbioticStaking(symbioticStaking).confirmedTimestampInfo(0); + + vm.startPrank(operatorA); + { + // reverts if submitted after deadline + vm.warp(block.timestamp + 12 hours); + vm.expectRevert("Job Expired"); + IJobManager(jobManager).submitProof(1, ""); + + vm.warp(block.timestamp - 12 hours); + IJobManager(jobManager).submitProof(1, ""); + } + vm.stopPrank(); + + /* + + fee paid: 1 usdc + + operator reward share: 30% + => 1 * 0.3 = 0.3 usdc + + transmitter comission rate: 20% + => 1 * 0.7 * 0.2 = 0.14 usdc + */ + assertEq(IERC20(feeToken).balanceOf(operatorA), 0.3 ether, "OperatorA fee reward mismatch"); + assertEq(IERC20(feeToken).balanceOf(transmitterA), 0.14 ether, "TransmitterA fee reward mismatch"); + } + + + function _vault_claim_reward_from_job_1() internal { + /* + Vault A claim fee reward + Inflation reward still in pending + */ + vm.startPrank(symbioticVaultA); + ISymbioticStakingReward(symbioticStakingReward).claimReward(operatorA); + vm.stopPrank(); + + /* + current status of staking: + operatorA: opted-into symbioticVaultA (weth) - 1000 weth, + operatorB: opted-into symbioticVaultA (weth) - 2000 weth, symbioticVaultB (pond) - 3000 pond + + operatorA has 100% reward share + + 1 USDC * 0.7(after operatorA commision 30%) * 0.8(after transmitter commision 20%) = 0.56 USDC + */ + + assertEq(IERC20(feeToken).balanceOf(symbioticVaultA), 0.56 ether, "SymbioticVaultA fee reward mismatch"); + } + + function _create_job_2() internal { + // requesterB creates a job + vm.startPrank(jobRequesterB); + { + // approve jobRequesterB -> feeToken + IERC20(feeToken).approve(jobManager, type(uint256).max); + uint256 jobmanagerBalanceBefore = IERC20(feeToken).balanceOf(jobManager); + + // pay 0.5 usdc as fee + IJobManager(jobManager).createJob(2, jobRequesterA, operatorA, 0.5 ether); + assertEq(IERC20(feeToken).balanceOf(jobManager) - jobmanagerBalanceBefore, 0.5 ether); + } + vm.stopPrank(); + } + + // inflation reward generated + function _submit_proof_job_2() internal { + vm.startPrank(operatorA); + { + IJobManager(jobManager).submitProof(2, ""); + } + vm.stopPrank(); + + /* + Inflation reward generated: 100 POND (1 job don by operator) + + OperatorA comission: 20% + => 100 * 0.3 = 30 POND + + TransmitterA comission: 20% + => 100 * 0.7(after operatorA comission) * 0.2(transmitter comission) = 14 POND + + SymbioticVaultA + */ + + // TODO: check inflation reward logic + } + + // job gets slashed + function _create_job_3() internal { + // requesterC creates a job + vm.startPrank(jobRequesterC); + { + IERC20(feeToken).approve(jobManager, type(uint256).max); + uint256 jobmanagerBalanceBefore = IERC20(feeToken).balanceOf(jobManager); + + // pay 2 usdc as fee + IJobManager(jobManager).createJob(3, jobRequesterC, operatorB, 2 ether); + assertEq(IERC20(feeToken).balanceOf(jobManager) - jobmanagerBalanceBefore, 2 ether); + } + vm.stopPrank(); + + // TODO: check job completion logic + } + + + function _slash_result_submission_job_3() internal { + uint256 jobRequesterCBalanceBefore = IERC20(feeToken).balanceOf(jobRequesterC); + + + // Partial Tx 1 + Struct.VaultSnapshot[] memory _vaultSnapshot = new Struct.VaultSnapshot[](3); + /* Vault A */ + // VaultA(1000 WETH) -> OperatorA + _vaultSnapshot[0].operator = operatorA; + _vaultSnapshot[0].vault = symbioticVaultA; + _vaultSnapshot[0].stakeToken = weth; + _vaultSnapshot[0].stakeAmount = VAULT_A_INTO_OPERATOR_A; + + /* Vault B */ + // VaultA(2000 weth) -> OperatorB + _vaultSnapshot[1].operator = operatorB; + _vaultSnapshot[1].vault = symbioticVaultA; + _vaultSnapshot[1].stakeToken = weth; + _vaultSnapshot[1].stakeAmount = VAULT_B_INTO_OPERATOR_A; + // VaultB(3000 POND) -> OperatorB + _vaultSnapshot[2].operator = operatorB; + _vaultSnapshot[2].vault = symbioticVaultB; + _vaultSnapshot[2].stakeToken = pond; + _vaultSnapshot[2].stakeAmount = VAULT_B_INTO_OPERATOR_B; + + Struct.JobSlashed[] memory _jobSlashed = new Struct.JobSlashed[](1); + + _jobSlashed[0].jobId = 3; + _jobSlashed[0].operator = operatorB; + _jobSlashed[0].rewardAddress = slasher; + + vm.startPrank(transmitterB); + { + // submit vault snapshot + ISymbioticStaking(symbioticStaking).submitVaultSnapshot(0, 1, abi.encode(block.timestamp, _vaultSnapshot), ""); + + // submit slash result + ISymbioticStaking(symbioticStaking).submitSlashResult(0, 1, abi.encode(block.timestamp, _jobSlashed), ""); + } + vm.stopPrank(); + // check if fee is refunded + assertEq(IERC20(feeToken).balanceOf(jobRequesterC) - jobRequesterCBalanceBefore, 2 ether, "JobRequesterC fee refund mismatch"); } } From 45d17de96b2b848745302e39d3e7ff13ed1f4b4f Mon Sep 17 00:00:00 2001 From: Frenchkebab Date: Mon, 14 Oct 2024 15:00:09 +0900 Subject: [PATCH 158/158] format import orders --- .../l2_contracts/InflationRewardManger.sol | 10 +++++++--- contracts/staking/l2_contracts/JobManager.sol | 15 ++++++++------- .../staking/l2_contracts/NativeStaking.sol | 18 +++++++++++------- .../staking/l2_contracts/StakingManager.sol | 12 ++++++++---- .../staking/l2_contracts/SymbioticStaking.sol | 12 ++++++++---- .../l2_contracts/SymbioticStakingReward.sol | 11 ++++++++--- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/contracts/staking/l2_contracts/InflationRewardManger.sol b/contracts/staking/l2_contracts/InflationRewardManger.sol index 591052e..5054b2f 100644 --- a/contracts/staking/l2_contracts/InflationRewardManger.sol +++ b/contracts/staking/l2_contracts/InflationRewardManger.sol @@ -1,20 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/* Parent Contracts */ import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; +/* Interfaces */ +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; -import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* Libraries */ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract InflationRewardManager is ContextUpgradeable, diff --git a/contracts/staking/l2_contracts/JobManager.sol b/contracts/staking/l2_contracts/JobManager.sol index 240a5af..b92c09a 100644 --- a/contracts/staking/l2_contracts/JobManager.sol +++ b/contracts/staking/l2_contracts/JobManager.sol @@ -1,26 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/* Contracts */ import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +/* Interfaces */ import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* Libraries */ import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/* - JobManager contract is responsible for creating and managing jobs. - Staking Manager contract is responsible for locking/unlocking tokens and distributing rewards. - */ contract JobManager is ContextUpgradeable, ERC165Upgradeable, diff --git a/contracts/staking/l2_contracts/NativeStaking.sol b/contracts/staking/l2_contracts/NativeStaking.sol index e1b4575..00ad05f 100644 --- a/contracts/staking/l2_contracts/NativeStaking.sol +++ b/contracts/staking/l2_contracts/NativeStaking.sol @@ -1,24 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/* Contracts */ import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +/* Interfaces */ import {INativeStaking} from "../../interfaces/staking/INativeStaking.sol"; -import {INativeStakingReward} from "../../interfaces/staking/INativeStakingReward.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* Libraries */ import {Struct} from "../../lib/staking/Struct.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + + contract NativeStaking is ContextUpgradeable, ERC165Upgradeable, diff --git a/contracts/staking/l2_contracts/StakingManager.sol b/contracts/staking/l2_contracts/StakingManager.sol index a8facd4..5e8000e 100644 --- a/contracts/staking/l2_contracts/StakingManager.sol +++ b/contracts/staking/l2_contracts/StakingManager.sol @@ -1,23 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/* Contracts */ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +/* Interfaces */ +import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {IStakingPool} from "../../interfaces/staking/IStakingPool.sol"; -import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; -import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* Libraries */ import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract StakingManager is ContextUpgradeable, diff --git a/contracts/staking/l2_contracts/SymbioticStaking.sol b/contracts/staking/l2_contracts/SymbioticStaking.sol index 324cea2..9100349 100644 --- a/contracts/staking/l2_contracts/SymbioticStaking.sol +++ b/contracts/staking/l2_contracts/SymbioticStaking.sol @@ -1,25 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +/* Contracts */ import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {JobManager} from "./JobManager.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +/* Interfaces */ +import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; import {IStakingManager} from "../../interfaces/staking/IStakingManager.sol"; import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; +import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; import {ISymbioticStakingReward} from "../../interfaces/staking/ISymbioticStakingReward.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/* Libraries */ import {Struct} from "../../lib/staking/Struct.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IRewardDistributor} from "../../interfaces/staking/IRewardDistributor.sol"; -import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; contract SymbioticStaking is ContextUpgradeable, ERC165Upgradeable, diff --git a/contracts/staking/l2_contracts/SymbioticStakingReward.sol b/contracts/staking/l2_contracts/SymbioticStakingReward.sol index 2f5e45f..7536c90 100644 --- a/contracts/staking/l2_contracts/SymbioticStakingReward.sol +++ b/contracts/staking/l2_contracts/SymbioticStakingReward.sol @@ -2,23 +2,28 @@ pragma solidity ^0.8.26; +/* Contracts */ import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {JobManager} from "./JobManager.sol"; +/* Interfaces */ import {IJobManager} from "../../interfaces/staking/IJobManager.sol"; -import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; import {IInflationRewardManager} from "../../interfaces/staking/IInflationRewardManager.sol"; +import {ISymbioticStaking} from "../../interfaces/staking/ISymbioticStaking.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* Libraries */ +import {Struct} from "../../lib/staking/Struct.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Struct} from "../../lib/staking/Struct.sol"; contract SymbioticStakingReward is ContextUpgradeable,