Skip to content

develop#38

Draft
zjesko wants to merge 40 commits intomainfrom
develop
Draft

develop#38
zjesko wants to merge 40 commits intomainfrom
develop

Conversation

@zjesko
Copy link
Copy Markdown

@zjesko zjesko commented Mar 2, 2025

Why this should be merged

How this works

How this was tested

How is this documented

zjesko and others added 23 commits February 11, 2025 20:26
* 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>
Comment on lines +305 to +327
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

Comment on lines +305 to +327
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

Comment on lines +305 to +327
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

Comment on lines +332 to +347
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);
}
Comment on lines +352 to +371
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

Comment on lines +352 to +371
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

Comment on lines +587 to +622
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);
}
}
Comment thread contracts/validator-manager/Native721TokenStakingManager.sol Fixed
Comment thread contracts/validator-manager/Native721TokenStakingManager.sol Fixed
Comment on lines +752 to +782
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

Native721TokenStakingManager._validateUptime(bytes32,uint32) has external calls inside a loop: (warpMessage,valid) = WARP_MESSENGER.getVerifiedWarpMessage(messageIndex)
Calls stack containing the loop:
Native721TokenStakingManager.submitUptimeProofs(bytes32[],uint32[])
Native721TokenStakingManager._updateUptime(bytes32,uint32)
Comment thread contracts/validator-manager/Native721TokenStakingManager.sol Fixed
* 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
@zjesko zjesko marked this pull request as draft March 7, 2025 07:24
* 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>
Comment on lines +407 to +408
function _reward(address account, uint256 amount) internal virtual override {
}

Check warning

Code scanning / Slither

Dead-code Warning

Native721TokenStakingManager._reward(address,uint256) is never used and should be removed
Comment on lines +652 to +690
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

Native721TokenStakingManager._updateUptime(bytes32,uint32) has external calls inside a loop: valWeight = $._manager.getValidator(validationID).startingWeight * validationUptime / dur
Calls stack containing the loop:
Native721TokenStakingManager.submitUptimeProofs(bytes32[],uint32[])
Comment on lines +697 to +739
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

Comment on lines +697 to +739
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

Comment on lines +754 to +771
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

Comment on lines +773 to +795
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

Comment on lines +16 to +29
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

Comment on lines +161 to +167
function registerNFTDelegation(
bytes32 validationID,
uint256[] memory tokenIDs
) external nonReentrant returns (bytes32) {
_lockNFTs(tokenIDs);
return _registerNFTDelegation(validationID, _msgSender(), tokenIDs);
}
Comment on lines +383 to +388
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

Native721TokenStakingManager._lockNFTs(uint256[]) has external calls inside a loop: _getERC721StakingManagerStorage()._token.safeTransferFrom(_msgSender(),address(this),tokenIDs[i])
Calls stack containing the loop:
Native721TokenStakingManager.registerNFTDelegation(bytes32,uint256[])
Comment on lines +383 to +388
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

Native721TokenStakingManager._lockNFTs(uint256[]) has external calls inside a loop: _getERC721StakingManagerStorage()._token.safeTransferFrom(_msgSender(),address(this),tokenIDs[i])
Calls stack containing the loop:
Native721TokenStakingManager.initiateValidatorRegistration(bytes,bytes,uint64,PChainOwner,PChainOwner,uint16,uint64,uint256[])
Native721TokenStakingManager._initiateValidatorRegistration(bytes,bytes,uint64,PChainOwner,PChainOwner,uint16,uint64,uint256,uint256[])
Comment on lines +397 to +401
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

Native721TokenStakingManager._unlockNFTs(address,uint256[]) has external calls inside a loop: _getERC721StakingManagerStorage()._token.transferFrom(address(this),to,tokenIDs[i])
Calls stack containing the loop:
Native721TokenStakingManager.completeNFTDelegatorRemoval(bytes32)
Comment on lines +397 to +401
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

Native721TokenStakingManager._unlockNFTs(address,uint256[]) has external calls inside a loop: _getERC721StakingManagerStorage()._token.transferFrom(address(this),to,tokenIDs[i])
Calls stack containing the loop:
Native721TokenStakingManager.unlockValidator(bytes32)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants