Conversation
* initilialiser checks and events * add complete delegator removal if validator has ended * add tests for double delegation and stake and nft delegation by same delegator * fix tests and add nft event * add locked nfts to event
* add validator removal admin * nit: fix extra space
* initial refactor * remove old deps * update staking manager * minor cleanup * minor cleanup * update imports * add working test * add more reward functions * update delegation uptime logic * update interface and comments (#36) * add working tests * remove console statements * clean update uptime function * reduce contract bytecode size * cleanup staking manager test * add tests * add more tests * improve coverage * minor security fixes --------- Co-authored-by: Tushar Jain <54453857+tushar994@users.noreply.github.com>
| function claimRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address[] memory tokens, | ||
| address recipient | ||
| ) external nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(block.timestamp < (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY){ | ||
| revert TooEarly(block.timestamp, (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY); | ||
| } | ||
|
|
||
| uint256[] memory rewards = getRewards(primary, epoch, tokens); | ||
| for(uint256 i = 0; i < tokens.length; i++){ | ||
| if(primary){ | ||
| $._rewardWithdrawn[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } else { | ||
| $._rewardWithdrawnNFT[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } | ||
| emit RewardClaimed(primary, epoch, _msgSender(), tokens[i], rewards[i]); | ||
| IERC20(tokens[i]).transfer(recipient, rewards[i]); | ||
| } | ||
| } |
Check failure
Code scanning / Slither
Unchecked transfer High
| function claimRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address[] memory tokens, | ||
| address recipient | ||
| ) external nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(block.timestamp < (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY){ | ||
| revert TooEarly(block.timestamp, (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY); | ||
| } | ||
|
|
||
| uint256[] memory rewards = getRewards(primary, epoch, tokens); | ||
| for(uint256 i = 0; i < tokens.length; i++){ | ||
| if(primary){ | ||
| $._rewardWithdrawn[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } else { | ||
| $._rewardWithdrawnNFT[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } | ||
| emit RewardClaimed(primary, epoch, _msgSender(), tokens[i], rewards[i]); | ||
| IERC20(tokens[i]).transfer(recipient, rewards[i]); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
| function claimRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address[] memory tokens, | ||
| address recipient | ||
| ) external nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(block.timestamp < (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY){ | ||
| revert TooEarly(block.timestamp, (epoch + 1) * $._epochDuration + REWARD_CLAIM_DELAY); | ||
| } | ||
|
|
||
| uint256[] memory rewards = getRewards(primary, epoch, tokens); | ||
| for(uint256 i = 0; i < tokens.length; i++){ | ||
| if(primary){ | ||
| $._rewardWithdrawn[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } else { | ||
| $._rewardWithdrawnNFT[epoch][_msgSender()][tokens[i]] += rewards[i]; | ||
| } | ||
| emit RewardClaimed(primary, epoch, _msgSender(), tokens[i], rewards[i]); | ||
| IERC20(tokens[i]).transfer(recipient, rewards[i]); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function registerRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address token, | ||
| uint256 amount | ||
| ) external onlyOwner nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(primary){ | ||
| $._rewardPools[epoch][token] = amount; | ||
| } else { | ||
| $._rewardPoolsNFT[epoch][token] = amount; | ||
| } | ||
| IERC20(token).transferFrom(_msgSender(), address(this), amount); | ||
| emit RewardRegistered(primary, epoch, token, amount); | ||
| } |
Check failure
Code scanning / Slither
Unchecked transfer High
| function cancelRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address token | ||
| ) external onlyOwner nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(block.timestamp >= epoch * $._epochDuration + REWARD_CLAIM_DELAY){ | ||
| revert TooLate(block.timestamp, epoch * $._epochDuration + REWARD_CLAIM_DELAY); | ||
| } | ||
|
|
||
| if(primary){ | ||
| IERC20(token).transfer(_msgSender(), $._rewardPools[epoch][token]); | ||
| $._rewardPools[epoch][token] = 0; | ||
| } else { | ||
| IERC20(token).transfer(_msgSender(), $._rewardPoolsNFT[epoch][token]); | ||
| $._rewardPoolsNFT[epoch][token] = 0; | ||
| } | ||
| emit RewardCancelled(primary, epoch, token); | ||
| } |
Check failure
Code scanning / Slither
Unchecked transfer High
| function cancelRewards( | ||
| bool primary, | ||
| uint64 epoch, | ||
| address token | ||
| ) external onlyOwner nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if(block.timestamp >= epoch * $._epochDuration + REWARD_CLAIM_DELAY){ | ||
| revert TooLate(block.timestamp, epoch * $._epochDuration + REWARD_CLAIM_DELAY); | ||
| } | ||
|
|
||
| if(primary){ | ||
| IERC20(token).transfer(_msgSender(), $._rewardPools[epoch][token]); | ||
| $._rewardPools[epoch][token] = 0; | ||
| } else { | ||
| IERC20(token).transfer(_msgSender(), $._rewardPoolsNFT[epoch][token]); | ||
| $._rewardPoolsNFT[epoch][token] = 0; | ||
| } | ||
| emit RewardCancelled(primary, epoch, token); | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function _initiateNFTDelegatorRemoval( | ||
| bytes32 delegationID | ||
| ) internal { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| Delegator memory delegator = $._delegatorStakes[delegationID]; | ||
| bytes32 validationID = delegator.validationID; | ||
|
|
||
| Validator memory validator = $._manager.getValidator(validationID); | ||
|
|
||
| // Ensure the delegator is active | ||
| if (delegator.status != DelegatorStatus.Active) { | ||
| revert InvalidDelegatorStatus(delegator.status); | ||
| } | ||
|
|
||
| if (delegator.owner != _msgSender()) { | ||
| revert UnauthorizedOwner(_msgSender()); | ||
| } | ||
|
|
||
| if (validator.status == ValidatorStatus.Active || validator.status == ValidatorStatus.Completed || validator.status == ValidatorStatus.PendingRemoved) { | ||
| // Check that minimum stake duration has passed. | ||
| if (validator.status != ValidatorStatus.Completed && block.timestamp < delegator.startTime + $._minimumStakeDuration) { | ||
| revert MinStakeDurationNotPassed(uint64(block.timestamp)); | ||
| } | ||
|
|
||
| $._delegatorStakes[delegationID].status = DelegatorStatus.PendingRemoved; | ||
| $._delegatorStakes[delegationID].endTime = uint64(block.timestamp); | ||
| emit InitiatedDelegatorRemoval(delegationID, validationID); | ||
| if (validator.status == ValidatorStatus.Completed) { | ||
| uint256[] memory tokenIDs = _completeNFTDelegatorRemoval(delegationID); | ||
| _unlockNFTs(delegator.owner, tokenIDs); | ||
| } | ||
| } else { | ||
| revert InvalidValidatorStatus(validator.status); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function _validateUptime(bytes32 validationID, uint32 messageIndex) internal view returns (uint64) { | ||
| if (!_isPoSValidator(validationID)) { | ||
| revert ValidatorNotPoS(validationID); | ||
| } | ||
|
|
||
| (WarpMessage memory warpMessage, bool valid) = | ||
| WARP_MESSENGER.getVerifiedWarpMessage(messageIndex); | ||
| if (!valid) { | ||
| revert InvalidWarpMessage(); | ||
| } | ||
|
|
||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
| // The uptime proof must be from the specifed uptime blockchain | ||
| if (warpMessage.sourceChainID != $._uptimeBlockchainID) { | ||
| revert InvalidWarpSourceChainID(warpMessage.sourceChainID); | ||
| } | ||
|
|
||
| // The sender is required to be the zero address so that we know the validator node | ||
| // signed the proof directly, rather than as an arbitrary on-chain message | ||
| if (warpMessage.originSenderAddress != address(0)) { | ||
| revert InvalidWarpOriginSenderAddress(warpMessage.originSenderAddress); | ||
| } | ||
|
|
||
| (bytes32 uptimeValidationID, uint64 uptime) = | ||
| ValidatorMessages.unpackValidationUptimeMessage(warpMessage.payload); | ||
| if (validationID != uptimeValidationID) { | ||
| revert UnexpectedValidationID(uptimeValidationID, validationID); | ||
| } | ||
|
|
||
| return uptime; | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
* audit fix: uptime refactor * add removed status to delegations * add delegation removal test * add delegation removal to redelegation * move max stake check * fix NFT test
* update staking manager * update staking manager * update staking manager * update staking manager * remove duplication * nit * fix tests * nit: minor fix * add more tests * minor fix * add more tests * minor fix * Uptime refactor (#42) * update uptime function * fix tests * fix tests * remove function * add epoch offset --------- Signed-off-by: Akshat Chhajer <akshat.c2k@gmail.com> * minor fix * minor fixes * minor fixes * add rewardresolved event * add uptimeKeeper * add natspec comments * audit fixes 2 (#44) * audit fix: BEAM-1 * audit fix: BEAM-2 * audit fix: BEAM-3 * audit fix: BEAM-5 * audit fix: BEAM-S1 * audit fix: BEAM-S3 * reduce bytecode size * update deploy scripts * remove dead code * update natspec comments --------- Signed-off-by: Akshat Chhajer <akshat.c2k@gmail.com>
| function _reward(address account, uint256 amount) internal virtual override { | ||
| } |
Check warning
Code scanning / Slither
Dead-code Warning
| function _updateUptime(bytes32 validationID, uint32 messageIndex) internal override returns (uint64) { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if ($._uptimeKeeper != _msgSender()) { | ||
| revert OwnableUnauthorizedAccount(_msgSender()); | ||
| } | ||
|
|
||
| uint64 uptime = _validateUptime(validationID, messageIndex); | ||
| uint64 epoch = _getEpoch() - 1; | ||
| uint64 dur = $._epochDuration; | ||
|
|
||
| PoSValidatorInfo storage validatorInfo = $._posValidatorInfo[validationID]; | ||
|
|
||
| if(validatorInfo.uptimeSeconds >= uptime ){ | ||
| return validatorInfo.uptimeSeconds; | ||
| } | ||
|
|
||
| uint256 validationUptime = uptime - validatorInfo.uptimeSeconds; | ||
| if (validationUptime * 100 / dur >= UPTIME_REWARDS_THRESHOLD_PERCENTAGE){ | ||
| validationUptime = dur; | ||
| } | ||
|
|
||
| // Calculate validator weights | ||
| uint256 valWeight = $._manager.getValidator(validationID).startingWeight * validationUptime / dur; | ||
| uint256 valWeightNFT = (validatorInfo.tokenIDs.length * 1e6) * validationUptime / dur; | ||
|
|
||
| // Update reward weights for validator owner | ||
| $._accountRewardWeight[epoch][validatorInfo.owner] += valWeight; | ||
| $._accountRewardWeightNFT[epoch][validatorInfo.owner] += valWeightNFT; | ||
| $._totalRewardWeight[epoch] += valWeight; | ||
| $._totalRewardWeightNFT[epoch] += valWeightNFT; | ||
|
|
||
| validatorInfo.uptimeSeconds = uptime; | ||
|
|
||
| $._validationUptimes[epoch][validationID] += validationUptime; | ||
|
|
||
| emit UptimeUpdated(validationID, uptime, epoch); | ||
| return uptime; | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
| function resolveRewards(bytes32[] memory delegationIDs) external { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if ($._uptimeKeeper != _msgSender()) { | ||
| revert OwnableUnauthorizedAccount(_msgSender()); | ||
| } | ||
|
|
||
| uint64 epoch = _getEpoch() - 1; | ||
| uint64 dur = $._epochDuration; | ||
|
|
||
| for (uint256 i = 0; i < delegationIDs.length; i++) { | ||
| Delegator memory delegator = $._delegatorStakes[delegationIDs[i]]; | ||
| PoSValidatorInfo storage validatorInfo = $._posValidatorInfo[delegator.validationID]; | ||
|
|
||
| uint64 epochStart = epoch * dur; | ||
| uint64 epochEnd = epochStart + dur; | ||
|
|
||
| uint64 delegationUptime; | ||
| { | ||
| uint64 delegationStart = uint64(Math.max(delegator.startTime, epochStart)); | ||
| uint64 delegationEnd = delegator.endTime != 0 ? delegator.endTime : epochEnd; | ||
| if (epochStart > delegationEnd){ continue; } | ||
| delegationUptime = uint64(Math.min(delegationEnd - delegationStart, $._validationUptimes[epoch][delegator.validationID])); | ||
|
|
||
| if (delegationUptime * 100 / dur >= UPTIME_REWARDS_THRESHOLD_PERCENTAGE){ | ||
| delegationUptime = dur; | ||
| } | ||
| } | ||
| uint256 delWeight = (delegator.weight * delegationUptime) / dur; | ||
| uint256 feeWeight = (delWeight * validatorInfo.delegationFeeBips) / BIPS_CONVERSION_FACTOR; | ||
|
|
||
| if ($._lockedNFTs[delegationIDs[i]].length == 0){ | ||
| $._accountRewardWeight[epoch][validatorInfo.owner] += feeWeight; | ||
| $._accountRewardWeight[epoch][delegator.owner] += delWeight - feeWeight; | ||
| $._totalRewardWeight[epoch] += delWeight; | ||
| } else { | ||
| $._accountRewardWeightNFT[epoch][validatorInfo.owner] += feeWeight; | ||
| $._accountRewardWeightNFT[epoch][delegator.owner] += delWeight - feeWeight; | ||
| $._totalRewardWeightNFT[epoch] += delWeight; | ||
| } | ||
| emit RewardResolved(delegationIDs[i], epoch); | ||
| } | ||
| } |
Check warning
Code scanning / Slither
Divide before multiply Medium
| function resolveRewards(bytes32[] memory delegationIDs) external { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
|
|
||
| if ($._uptimeKeeper != _msgSender()) { | ||
| revert OwnableUnauthorizedAccount(_msgSender()); | ||
| } | ||
|
|
||
| uint64 epoch = _getEpoch() - 1; | ||
| uint64 dur = $._epochDuration; | ||
|
|
||
| for (uint256 i = 0; i < delegationIDs.length; i++) { | ||
| Delegator memory delegator = $._delegatorStakes[delegationIDs[i]]; | ||
| PoSValidatorInfo storage validatorInfo = $._posValidatorInfo[delegator.validationID]; | ||
|
|
||
| uint64 epochStart = epoch * dur; | ||
| uint64 epochEnd = epochStart + dur; | ||
|
|
||
| uint64 delegationUptime; | ||
| { | ||
| uint64 delegationStart = uint64(Math.max(delegator.startTime, epochStart)); | ||
| uint64 delegationEnd = delegator.endTime != 0 ? delegator.endTime : epochEnd; | ||
| if (epochStart > delegationEnd){ continue; } | ||
| delegationUptime = uint64(Math.min(delegationEnd - delegationStart, $._validationUptimes[epoch][delegator.validationID])); | ||
|
|
||
| if (delegationUptime * 100 / dur >= UPTIME_REWARDS_THRESHOLD_PERCENTAGE){ | ||
| delegationUptime = dur; | ||
| } | ||
| } | ||
| uint256 delWeight = (delegator.weight * delegationUptime) / dur; | ||
| uint256 feeWeight = (delWeight * validatorInfo.delegationFeeBips) / BIPS_CONVERSION_FACTOR; | ||
|
|
||
| if ($._lockedNFTs[delegationIDs[i]].length == 0){ | ||
| $._accountRewardWeight[epoch][validatorInfo.owner] += feeWeight; | ||
| $._accountRewardWeight[epoch][delegator.owner] += delWeight - feeWeight; | ||
| $._totalRewardWeight[epoch] += delWeight; | ||
| } else { | ||
| $._accountRewardWeightNFT[epoch][validatorInfo.owner] += feeWeight; | ||
| $._accountRewardWeightNFT[epoch][delegator.owner] += delWeight - feeWeight; | ||
| $._totalRewardWeightNFT[epoch] += delWeight; | ||
| } | ||
| emit RewardResolved(delegationIDs[i], epoch); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function unlockDelegator(bytes32 delegationID) external nonReentrant { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
| Delegator memory delegator = $._delegatorStakes[delegationID]; | ||
|
|
||
| if (delegator.status != DelegatorStatus.Removed || $._unlocked[delegationID]) { | ||
| revert InvalidDelegatorStatus(delegator.status); | ||
| } | ||
|
|
||
| if(delegator.startTime != 0 && block.timestamp < delegator.endTime + $._unlockDuration) { | ||
| revert UnlockDurationNotPassed(uint64(block.timestamp)); | ||
| } | ||
|
|
||
| $._unlocked[delegationID] = true; | ||
|
|
||
| emit UnlockedDelegation(delegationID); | ||
| // Unlock the delegator's stake. | ||
| _unlock(delegator.owner, weightToValue(delegator.weight)); | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function _unlockValidator(bytes32 validationID) internal { | ||
| StakingManagerStorage storage $ = _getStakingManagerStorage(); | ||
| Validator memory validator = $._manager.getValidator(validationID); | ||
|
|
||
| if ((validator.status != ValidatorStatus.Completed && validator.status != ValidatorStatus.Invalidated) | ||
| || $._unlocked[validationID]) { | ||
| revert InvalidValidatorStatus(validator.status); | ||
| } | ||
|
|
||
| if (!_isPoSValidator(validationID)) { | ||
| revert ValidatorNotPoS(validationID); | ||
| } | ||
|
|
||
| if(validator.startTime != 0 && block.timestamp < validator.endTime + $._unlockDuration) { | ||
| revert UnlockDurationNotPassed(uint64(block.timestamp)); | ||
| } | ||
|
|
||
| $._unlocked[validationID] = true; | ||
|
|
||
| emit UnlockedValidation(validationID); | ||
| // The stake is unlocked whether the validation period is completed or invalidated. | ||
| _unlock($._posValidatorInfo[validationID].owner, weightToValue(validator.startingWeight)); | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function run() external { | ||
| vm.startBroadcast(); | ||
|
|
||
| Native721TokenStakingManager stakingManager = Native721TokenStakingManager(0xF4B5869AabE19a106C0df25E1537d855b54EEcBD); | ||
|
|
||
| // ExampleERC20 rewardToken = new ExampleERC20(); | ||
| ExampleERC20 rewardToken = ExampleERC20(0xFE42fC7Ac06ad13BD54aA3010E2f5e9e0DF6D752); | ||
| rewardToken.approve(address(stakingManager), 4000e18); | ||
|
|
||
| stakingManager.registerRewards(true, 10084, address(rewardToken), 2000e18); | ||
| stakingManager.registerRewards(false, 10084, address(rewardToken), 2000e18); | ||
|
|
||
| vm.stopBroadcast(); | ||
| } |
Check warning
Code scanning / Slither
Unused return Medium
| function registerNFTDelegation( | ||
| bytes32 validationID, | ||
| uint256[] memory tokenIDs | ||
| ) external nonReentrant returns (bytes32) { | ||
| _lockNFTs(tokenIDs); | ||
| return _registerNFTDelegation(validationID, _msgSender(), tokenIDs); | ||
| } |
Check notice
Code scanning / Slither
Reentrancy vulnerabilities Low
| function _lockNFTs(uint256[] memory tokenIDs) internal returns (uint256) { | ||
| for (uint256 i = 0; i < tokenIDs.length; i++) { | ||
| _getERC721StakingManagerStorage()._token.safeTransferFrom(_msgSender(), address(this), tokenIDs[i]); | ||
| } | ||
| return tokenIDs.length; | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
| function _lockNFTs(uint256[] memory tokenIDs) internal returns (uint256) { | ||
| for (uint256 i = 0; i < tokenIDs.length; i++) { | ||
| _getERC721StakingManagerStorage()._token.safeTransferFrom(_msgSender(), address(this), tokenIDs[i]); | ||
| } | ||
| return tokenIDs.length; | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
| function _unlockNFTs(address to, uint256[] memory tokenIDs) internal virtual { | ||
| for (uint256 i = 0; i < tokenIDs.length; i++) { | ||
| _getERC721StakingManagerStorage()._token.transferFrom(address(this), to, tokenIDs[i]); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
| function _unlockNFTs(address to, uint256[] memory tokenIDs) internal virtual { | ||
| for (uint256 i = 0; i < tokenIDs.length; i++) { | ||
| _getERC721StakingManagerStorage()._token.transferFrom(address(this), to, tokenIDs[i]); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Calls inside a loop Low
Why this should be merged
How this works
How this was tested
How is this documented