Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
457 changes: 457 additions & 0 deletions abi/MigratorV6Epochs9to12Rewards.json

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions contracts/migrations/MigratorV6Epochs9to12Rewards.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import {StakingStorage} from "../storage/StakingStorage.sol";
import {ShardingTableStorage} from "../storage/ShardingTableStorage.sol";
import {ParametersStorage} from "../storage/ParametersStorage.sol";
import {Ask} from "../Ask.sol";
import {ShardingTable} from "../ShardingTable.sol";
import {DelegatorsInfo} from "../storage/DelegatorsInfo.sol";
import {ContractStatus} from "../abstract/ContractStatus.sol";
import {INamed} from "../interfaces/INamed.sol";
import {IVersioned} from "../interfaces/IVersioned.sol";
import {Chronos} from "../storage/Chronos.sol";
import {Staking} from "../Staking.sol";
import {HubLib} from "../libraries/HubLib.sol";
import {ICustodian} from "../interfaces/ICustodian.sol";
import {ProfileStorage} from "../storage/ProfileStorage.sol";
import {ProfileLib} from "../libraries/ProfileLib.sol";

contract MigratorV6Epochs9to12Rewards is INamed, IVersioned, ContractStatus {
string private constant _NAME = "MigratorV6Epochs9to12Rewards";
string private constant _VERSION = "1.0.0";
uint256 public constant START_EPOCH = 9;
uint256 public constant END_EPOCH = 12;
Comment on lines +23 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of these 2? I don't see them used in the contract anywhere

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused START_EPOCH and END_EPOCH constants in contract

Low Severity

START_EPOCH and END_EPOCH are declared as public constants but never referenced in any contract logic. No function uses them to gate or validate behavior, so there is no on-chain enforcement that this migrator is restricted to epochs 9–12. This aligns with the reviewer's question ("What is the purpose of these 2?"). If the constants are purely informational metadata, they add dead code; if they were intended to enforce epoch boundaries, the enforcement logic is missing.

Fix in Cursor Fix in Web


mapping(uint72 => mapping(address => uint96)) public delegatorRewardAmount;
mapping(uint72 => mapping(address => bool)) public claimedDelegatorReward;

mapping(uint72 => uint96) public operatorRewardAmount;
mapping(uint72 => bool) public claimedOperatorReward;

StakingStorage public stakingStorage;
ShardingTableStorage public shardingTableStorage;
ShardingTable public shardingTable;
ParametersStorage public parametersStorage;
Ask public askContract;
DelegatorsInfo public delegatorsInfo;
Chronos public chronos;
Staking public staking;
ProfileStorage public profileStorage;

// @dev Only transactions by HubController owner or one of the owners of the MultiSig Wallet
modifier onlyOwnerOrMultiSigOwner() {
_checkOwnerOrMultiSigOwner();
_;
}

modifier profileExists(uint72 identityId) {
_checkProfileExists(identityId);
_;
}

// solhint-disable-next-line no-empty-blocks
constructor(address hubAddress) ContractStatus(hubAddress) {}

function initialize() external onlyHub {
stakingStorage = StakingStorage(hub.getContractAddress("StakingStorage"));
shardingTableStorage = ShardingTableStorage(hub.getContractAddress("ShardingTableStorage"));
shardingTable = ShardingTable(hub.getContractAddress("ShardingTable"));
parametersStorage = ParametersStorage(hub.getContractAddress("ParametersStorage"));
askContract = Ask(hub.getContractAddress("Ask"));
delegatorsInfo = DelegatorsInfo(hub.getContractAddress("DelegatorsInfo"));
chronos = Chronos(hub.getContractAddress("Chronos"));
staking = Staking(hub.getContractAddress("Staking"));
profileStorage = ProfileStorage(hub.getContractAddress("ProfileStorage"));
}

function name() external pure override returns (string memory) {
return _NAME;
}

function version() external pure override returns (string memory) {
return _VERSION;
}

event DelegatorRewardAmountSet(uint72 indexed identityId, address indexed delegator, uint96 amount);
event OperatorRewardAmountSet(uint72 indexed identityId, uint96 amount);

function setDelegatorRewardAmount(
uint72 identityId,
address delegator,
uint96 amount
) external onlyOwnerOrMultiSigOwner profileExists(identityId) {
require(amount > 0, "No reward");

Check warning on line 84 in contracts/migrations/MigratorV6Epochs9to12Rewards.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Use Custom Errors instead of require statements
delegatorRewardAmount[identityId][delegator] = amount;

emit DelegatorRewardAmountSet(identityId, delegator, amount);
}

function setOperatorRewardAmount(
uint72 identityId,
uint96 amount
) external onlyOwnerOrMultiSigOwner profileExists(identityId) {
require(amount > 0, "No reward");

Check warning on line 94 in contracts/migrations/MigratorV6Epochs9to12Rewards.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Use Custom Errors instead of require statements
operatorRewardAmount[identityId] = amount;

emit OperatorRewardAmountSet(identityId, amount);
}

/**
* @notice Claims the pre-calculated reward for the caller and immediately
* restakes it for the given node.
* @param identityId The node identifier the caller is delegating to.
* @param delegator The delegator address.
*/
function migrateDelegatorReward(uint72 identityId, address delegator) external profileExists(identityId) {
require(!claimedDelegatorReward[identityId][delegator], "Already claimed delegator reward for this node");

uint96 amount = delegatorRewardAmount[identityId][delegator];
require(amount > 0, "No reward");

// Mark reward as processed - avoid reentrancy
claimedDelegatorReward[identityId][delegator] = true;

// Validate epoch claims for V8 rewards
staking.validateDelegatorEpochClaims(identityId, delegator);

bytes32 delegatorKey = keccak256(abi.encodePacked(delegator));

// Settle pending score changes in V8 system
staking.prepareForStakeChange(chronos.getCurrentEpoch(), identityId, delegatorKey);

uint96 currentDelegatorStakeBase = stakingStorage.getDelegatorStakeBase(identityId, delegatorKey);
uint96 newDelegatorStakeBase = currentDelegatorStakeBase + amount;

uint96 totalNodeStakeBefore = stakingStorage.getNodeStake(identityId);
uint96 totalNodeStakeAfter = totalNodeStakeBefore + amount;

// Update staking balances
stakingStorage.setDelegatorStakeBase(identityId, delegatorKey, newDelegatorStakeBase);
stakingStorage.setNodeStake(identityId, totalNodeStakeAfter);
stakingStorage.increaseTotalStake(amount);

_addNodeToShardingTable(identityId, totalNodeStakeAfter);
askContract.recalculateActiveSet();

_manageDelegatorStatus(identityId, delegator);
}

/**
* @notice Transfers the operator reward to the operator balance in staking storage.
* @param identityId The node identifier the caller is delegating to.
*/
function migrateOperatorReward(uint72 identityId) external profileExists(identityId) {
require(!claimedOperatorReward[identityId], "Already claimed operator reward for this node");

uint96 amount = operatorRewardAmount[identityId];
require(amount > 0, "No reward");

claimedOperatorReward[identityId] = true;

stakingStorage.increaseOperatorFeeBalance(identityId, amount);
}

function _manageDelegatorStatus(uint72 identityId, address delegator) internal {
if (!delegatorsInfo.isNodeDelegator(identityId, delegator)) {
delegatorsInfo.addDelegator(identityId, delegator);
}
uint256 lastStakeHeldEpoch = delegatorsInfo.getLastStakeHeldEpoch(identityId, delegator);
if (lastStakeHeldEpoch > 0) {
delegatorsInfo.setLastStakeHeldEpoch(identityId, delegator, 0);
}
}

function _addNodeToShardingTable(uint72 identityId, uint96 totalNodeStakeAfter) internal {
if (!shardingTableStorage.nodeExists(identityId)) {
if (totalNodeStakeAfter >= parametersStorage.minimumStake()) {
shardingTable.insertNode(identityId);
}
}
}

function _isMultiSigOwner(address multiSigAddress) internal view returns (bool) {
try ICustodian(multiSigAddress).getOwners() returns (address[] memory multiSigOwners) {
for (uint256 i = 0; i < multiSigOwners.length; i++) {
if (msg.sender == multiSigOwners[i]) {
return true;
}
} // solhint-disable-next-line no-empty-blocks
} catch {}

return false;
}

function _checkOwnerOrMultiSigOwner() internal view virtual {
address hubOwner = hub.owner();
if (msg.sender != hubOwner && !_isMultiSigOwner(hubOwner)) {
revert HubLib.UnauthorizedAccess("Only Hub Owner or Multisig Owner");
}
}

/**
* @dev Internal function to validate that a node profile exists
* Used by modifiers and functions to ensure operations target valid nodes
* @param identityId Node identity to check existence for
*/
function _checkProfileExists(uint72 identityId) internal view virtual {
if (!profileStorage.profileExists(identityId)) {
revert ProfileLib.ProfileDoesntExist(identityId);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entire contract duplicates existing migrator logic

Low Severity

MigratorV6Epochs9to12Rewards is a near-complete copy of MigratorV6TuningPeriodRewards (and MigratorV8TuningPeriodRewards), with the only addition being START_EPOCH and END_EPOCH constants. All internal helper functions (_manageDelegatorStatus, _addNodeToShardingTable, _isMultiSigOwner, _checkOwnerOrMultiSigOwner, _checkProfileExists) and the complete migrateDelegatorReward/migrateOperatorReward flows are duplicated. Extracting the shared logic into a base contract would reduce maintenance burden and the risk of inconsistent bug fixes across three+ copies.

Fix in Cursor Fix in Web

26 changes: 26 additions & 0 deletions deploy/034_deploy_migrator_v6_epochs_9to12_rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { DeployFunction } from 'hardhat-deploy/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
await hre.helpers.deploy({
newContractName: 'MigratorV6Epochs9to12Rewards',
});
};

export default func;
func.tags = ['MigratorV6Epochs9to12Rewards'];
func.dependencies = [
'Hub',
'StakingStorage',
'ShardingTableStorage',
'ShardingTable',
'ParametersStorage',
'Ask',
'DelegatorsInfo',
'Chronos',
'Staking',
'ProfileStorage',
'IdentityStorage',
'Token',
'Profile',
];
9 changes: 9 additions & 0 deletions deployments/base_sepolia_test_contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@
"deploymentBlock": 37268394,
"deploymentTimestamp": 1770305077619,
"deployed": true
},
"MigratorV6Epochs9to12Rewards": {
"evmAddress": "0xb59d9CF1c8a028F2D84F4C40AfC25895de47AAC9",
"version": "1.0.0",
"gitBranch": "migrator-v6-epoch-9-to-12",
"gitCommitHash": "82a95af5a4e7f53b9e3fab5f227fccde8c5a6ec3",
"deploymentBlock": 37566046,
"deploymentTimestamp": 1770900381140,
"deployed": true
}
}
}
9 changes: 9 additions & 0 deletions deployments/gnosis_chiado_test_contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@
"deploymentBlock": 19690369,
"deploymentTimestamp": 1770306024120,
"deployed": true
},
"MigratorV6Epochs9to12Rewards": {
"evmAddress": "0x05F3502486090F0e5A9F0831298ea775ad6d24B6",
"version": "1.0.0",
"gitBranch": "migrator-v6-epoch-9-to-12",
"gitCommitHash": "82a95af5a4e7f53b9e3fab5f227fccde8c5a6ec3",
"deploymentBlock": 20015829,
"deploymentTimestamp": 1770900422736,
"deployed": true
}
}
}
10 changes: 10 additions & 0 deletions deployments/neuroweb_testnet_contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,16 @@
"deploymentBlock": 10492663,
"deploymentTimestamp": 1770307855920,
"deployed": true
},
"MigratorV6Epochs9to12Rewards": {
"evmAddress": "0x7388F16BeBC5fc790bC7294E6ee9D6Da5d45Ab79",
"substrateAddress": "5EMjsczjKqNXr9ohkphd1ZwwUdmbjQg9oXewaATnukBKzdE5",
"version": "1.0.0",
"gitBranch": "migrator-v6-epoch-9-to-12",
"gitCommitHash": "82a95af5a4e7f53b9e3fab5f227fccde8c5a6ec3",
"deploymentBlock": 10567578,
"deploymentTimestamp": 1770900734826,
"deployed": true
}
}
}
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ config.networks = {
chainId: 20430,
url: rpc('neuroweb_testnet'),
gas: 10_000_000, // Gas limit used for deploys
gasPrice: 20,
gasPrice: 1_000_000,
accounts: accounts('neuroweb_testnet'),
saveDeployments: false,
},
Expand Down
Loading
Loading