Skip to content
Merged
56 changes: 56 additions & 0 deletions abi/SafeCast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[
{
"inputs": [
{
"internalType": "uint8",
"name": "bits",
"type": "uint8"
},
{
"internalType": "int256",
"name": "value",
"type": "int256"
}
],
"name": "SafeCastOverflowedIntDowncast",
"type": "error"
},
{
"inputs": [
{
"internalType": "int256",
"name": "value",
"type": "int256"
}
],
"name": "SafeCastOverflowedIntToUint",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "bits",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "SafeCastOverflowedUintDowncast",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "SafeCastOverflowedUintToInt",
"type": "error"
}
]
101 changes: 20 additions & 81 deletions contracts/RandomSampling.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable {
string private constant _NAME = "RandomSampling";
string private constant _VERSION = "1.0.0";
string private constant _VERSION = "1.0.1-freeze";
uint256 public constant SCALE18 = 1e18;

IdentityStorage public identityStorage;
Expand Down Expand Up @@ -139,45 +139,31 @@ contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable {

/**
* @dev Creates a new challenge for the calling node in the current proofing period
* Caller must have a registered profile and cannot have an active unsolved challenge
* Generates a random knowledge collection and chunk to be proven
* Can only create one challenge per proofing period
* V8 -> V10 freeze: this function is a silent no-op on this contract version.
* Score accumulation has stopped on V8; nodes should migrate to V10.
* Returns early without writing to RandomSamplingStorage so that running
* proofing loops on V8 nodes do not fail their transactions.
*/
function createChallenge()
external
profileExists(identityStorage.getIdentityId(msg.sender))
nodeExistsInShardingTable(identityStorage.getIdentityId(msg.sender))
{
uint72 identityId = identityStorage.getIdentityId(msg.sender);

RandomSamplingLib.Challenge memory nodeChallenge = randomSamplingStorage.getNodeChallenge(identityId);

if (nodeChallenge.activeProofPeriodStartBlock == updateAndGetActiveProofPeriodStartBlock()) {
// Revert if node has already solved the challenge for this period
if (nodeChallenge.solved) {
revert("The challenge for this proof period has already been solved");
}

// Revert if a challenge for this node exists but has not been solved yet
if (nodeChallenge.knowledgeCollectionId != 0) {
revert("An unsolved challenge already exists for this node in the current proof period");
}
}

// Generate a new challenge
RandomSamplingLib.Challenge memory challenge = _generateChallenge(msg.sender);

// Store the new challenge in the storage contract
randomSamplingStorage.setNodeChallenge(identityId, challenge);
// V8 -> V10 freeze: silent no-op. No new challenges are generated and
// no writes are made to RandomSamplingStorage. Existing on-chain
// challenges remain accessible via the storage contract for historical
// inspection but cannot be progressed via this contract version.
}

/**
* @dev Submits proof for an active challenge to earn score used for later reward calculation
* Validates the submitted chunk and merkle proof against the expected Merkle root
* On successful proof: marks challenge as solved, increments valid proofs count,
* calculates and adds node score, and updates epoch scoring data
* @param chunk The data chunk being proven (must match challenge requirements)
* @param merkleProof Array of hashes for Merkle proof verification
* @dev Submits proof for an active challenge to earn score.
* V8 -> V10 freeze: this function is a silent no-op on this contract
* version. No score is added, no events are emitted, and no
* RandomSamplingStorage state is mutated. Submission attempts succeed
* (rather than revert) so that running V8 node proof loops do not
* generate failed-tx noise during the migration window.
* @param chunk Unused under freeze. Retained for ABI compatibility.
* @param merkleProof Unused under freeze. Retained for ABI compatibility.
*/
function submitProof(
string memory chunk,
Expand All @@ -187,56 +173,9 @@ contract RandomSampling is INamed, IVersioned, ContractStatus, IInitializable {
profileExists(identityStorage.getIdentityId(msg.sender))
nodeExistsInShardingTable(identityStorage.getIdentityId(msg.sender))
{
// Get node identityId
uint72 identityId = identityStorage.getIdentityId(msg.sender);

// Get node challenge
RandomSamplingLib.Challenge memory challenge = randomSamplingStorage.getNodeChallenge(identityId);

if (challenge.solved) {
revert("This challenge has already been solved");
}

uint256 activeProofPeriodStartBlock = updateAndGetActiveProofPeriodStartBlock();

// verify that the challengeId matches the current challenge
if (challenge.activeProofPeriodStartBlock != activeProofPeriodStartBlock) {
revert("This challenge is no longer active");
}

// Construct the merkle root from chunk and merkleProof
bytes32 computedMerkleRoot = _computeMerkleRootFromProof(chunk, challenge.chunkId, merkleProof);

// Get the expected merkle root for this challenge
bytes32 expectedMerkleRoot = knowledgeCollectionStorage.getLatestMerkleRoot(challenge.knowledgeCollectionId);

// Verify the submitted root matches
if (computedMerkleRoot == expectedMerkleRoot) {
// Mark as correct submission and add points to the node
challenge.solved = true;
randomSamplingStorage.setNodeChallenge(identityId, challenge);

uint256 epoch = chronos.getCurrentEpoch();
randomSamplingStorage.incrementEpochNodeValidProofsCount(epoch, identityId);
uint256 score18 = calculateNodeScore(identityId);
randomSamplingStorage.addToNodeEpochProofPeriodScore(
epoch,
activeProofPeriodStartBlock,
identityId,
score18
);
randomSamplingStorage.addToNodeEpochScore(epoch, identityId, score18);
randomSamplingStorage.addToAllNodesEpochScore(epoch, score18);

// Calculate and add to nodeEpochScorePerStake
uint96 totalNodeStake = stakingStorage.getNodeStake(identityId);
if (totalNodeStake > 0) {
uint256 nodeScorePerStake36 = (score18 * SCALE18) / totalNodeStake;
randomSamplingStorage.addToNodeEpochScorePerStake(epoch, identityId, nodeScorePerStake36);
}
} else {
revert MerkleRootMismatchError(computedMerkleRoot, expectedMerkleRoot);
}
// V8 -> V10 freeze: silent no-op.
chunk;
merkleProof;
}

/**
Expand Down
65 changes: 21 additions & 44 deletions contracts/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {EpochStorage} from "./storage/EpochStorage.sol";

contract Staking is INamed, IVersioned, ContractStatus, IInitializable {
string private constant _NAME = "Staking";
string private constant _VERSION = "1.0.1";
string private constant _VERSION = "1.0.2-freeze";
uint256 public constant SCALE18 = 1e18;
uint256 private constant EPOCH_POOL_INDEX = 1;

Expand Down Expand Up @@ -554,38 +554,27 @@ contract Staking is INamed, IVersioned, ContractStatus, IInitializable {
"Already claimed rewards for this epoch"
);

// settle all pending score changes for the node's delegator
uint256 delegatorScore18 = _prepareForStakeChange(epoch, identityId, delegatorKey);
// V8 -> V10 freeze: still settle pending score changes (idempotent
// bookkeeping) so subsequent stake/withdraw ops see a consistent
// score-per-stake index. Discard the returned delegator score - it
// is unused under the freeze.
_prepareForStakeChange(epoch, identityId, delegatorKey);
uint256 nodeScore18 = randomSamplingStorage.getNodeEpochScore(epoch, identityId);
uint256 reward;

// If nodeScore18 = 0, rewards are 0 too
// V8 -> V10 freeze: epoch pools are no longer backed by escrowed TRAC.
// The reward stream is forced to zero on this contract version. All
// surrounding bookkeeping (lastClaimedEpoch, hasDelegatorClaimedEpochRewards,
// isOperatorFeeClaimedForEpoch, netNodeEpochRewards) keeps advancing so
// that downstream invariants relied on by Profile.updateOperatorFee and
// _validateDelegatorEpochClaims remain intact.
if (nodeScore18 > 0) {
// netNodeRewards (rewards for node's delegators) = grossNodeRewards - operator fee
uint256 netNodeRewards;
uint256 netNodeRewards = 0;
if (!delegatorsInfo.isOperatorFeeClaimedForEpoch(identityId, epoch)) {
// Operator fee has not been claimed for this epoch, calculate it
uint256 allNodesScore18 = randomSamplingStorage.getAllNodesEpochScore(epoch);
if (allNodesScore18 > 0) {
uint256 grossNodeRewards = (epochStorage.getEpochPool(EPOCH_POOL_INDEX, epoch) * nodeScore18) /
allNodesScore18;
uint96 operatorFeeAmount = uint96(
(grossNodeRewards * profileStorage.getLatestOperatorFeePercentage(identityId)) /
parametersStorage.maxOperatorFee()
);
netNodeRewards = grossNodeRewards - operatorFeeAmount;
// Mark the operator fee as claimed for this epoch
delegatorsInfo.setIsOperatorFeeClaimedForEpoch(identityId, epoch, true);
// Set node's delegators net rewards for this epoch so we don't have to calculate it again
delegatorsInfo.setNetNodeEpochRewards(identityId, epoch, netNodeRewards);
stakingStorage.increaseOperatorFeeBalance(identityId, operatorFeeAmount);
}
} else {
// Operator fee has been claimed for this epoch already, use the previously calculated node's delegators net rewards for this epoch
netNodeRewards = delegatorsInfo.getNetNodeEpochRewards(identityId, epoch);
delegatorsInfo.setIsOperatorFeeClaimedForEpoch(identityId, epoch, true);
delegatorsInfo.setNetNodeEpochRewards(identityId, epoch, netNodeRewards);
}

reward = (delegatorScore18 * netNodeRewards) / nodeScore18;
reward = 0;
}

// If the operator fee flag has not been set for the epoch (because it had no score), set it now.
Expand All @@ -597,7 +586,6 @@ contract Staking is INamed, IVersioned, ContractStatus, IInitializable {
// update state even when reward is zero
// Mark the delegator's rewards as claimed for this epoch
delegatorsInfo.setHasDelegatorClaimedEpochRewards(epoch, identityId, delegatorKey, true);
uint256 lastClaimedEpoch = delegatorsInfo.getLastClaimedEpoch(identityId, delegator);
delegatorsInfo.setLastClaimedEpoch(identityId, delegator, epoch);

// Check if this completes all required claims and reset lastStakeHeldEpoch
Expand All @@ -612,22 +600,11 @@ contract Staking is INamed, IVersioned, ContractStatus, IInitializable {
}
}

uint256 rolling = delegatorsInfo.getDelegatorRollingRewards(identityId, delegator);

if (reward == 0 && rolling == 0) return;

// if there are still older epochs pending, accumulate; otherwise restake immediately
if ((currentEpoch - 1) - lastClaimedEpoch > 1) {
delegatorsInfo.setDelegatorRollingRewards(identityId, delegator, rolling + reward);
} else {
uint96 total = uint96(reward + rolling);
delegatorsInfo.setDelegatorRollingRewards(identityId, delegator, 0);
stakingStorage.increaseDelegatorStakeBase(identityId, delegatorKey, total);
stakingStorage.increaseNodeStake(identityId, total);
stakingStorage.increaseTotalStake(total);
}
//Should it increase on roling rewards or on stakeBaseIncrease only?
stakingStorage.addDelegatorCumulativeEarnedRewards(identityId, delegatorKey, uint96(reward));
// V8 -> V10 freeze: do not apply previously accumulated rolling rewards
// either. Any non-zero `delegatorsInfo.delegatorRollingRewards` value
// is preserved in storage untouched (so V10 reconciliation can read it),
// but it is never paid into the delegator's stakeBase / nodeStake /
// totalStake on this contract version.
}

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

// MigratorV6Epochs9to12Rewards is managed via a separate operational path on
// these mainnets and must not be (re-)deployed by the standard pipeline here.
// Remove the network from the skip set only when the operational handling
// is updated accordingly.
const SKIP_NETWORKS = new Set(['neuroweb_mainnet', 'gnosis_mainnet']);

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
if (SKIP_NETWORKS.has(hre.network.name)) {
console.log(
`Skipping MigratorV6Epochs9to12Rewards deploy on ${hre.network.name} (managed separately).`,
);
return;
}

await hre.helpers.deploy({
newContractName: 'MigratorV6Epochs9to12Rewards',
});
Expand Down
33 changes: 21 additions & 12 deletions deployments/base_mainnet_contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@
"deployed": true
},
"Staking": {
"evmAddress": "0x4CB817146a51d0C2C8253EA38E6BDA133C9fA547",
"version": "1.0.1",
"gitBranch": "main",
"gitCommitHash": "99bbb9b016d2053c0b4381c066d01b59e77a9ca1",
"deploymentBlock": 35877884,
"deploymentTimestamp": 1758545118117,
"evmAddress": "0xDaa40BEb1D73bC43E437cDC3188abE565119619d",
"version": "1.0.2-freeze",
"gitBranch": "release/v8-v10-freeze-from-main",
"gitCommitHash": "c05a10cee76b88c08507142e2567cd1d941e27e6",
"deploymentBlock": 45730857,
"deploymentTimestamp": 1778251062695,
"deployed": true
},
"Profile": {
Expand Down Expand Up @@ -312,12 +312,12 @@
"deployed": true
},
"RandomSampling": {
"evmAddress": "0x4B20D581695fb96db8FE78D8b12994fa43946eFA",
"version": "1.0.0",
"gitBranch": "RFC-26-update",
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
"deploymentBlock": 41853400,
"deploymentTimestamp": 1770496150937,
"evmAddress": "0xA32780d6A89542462271ca6d2d78373889F1C2d9",
"version": "1.0.1-freeze",
"gitBranch": "release/v8-v10-freeze-from-main",
"gitCommitHash": "c05a10cee76b88c08507142e2567cd1d941e27e6",
"deploymentBlock": 45730860,
"deploymentTimestamp": 1778251068573,
"deployed": true
},
"MigratorV8TuningPeriodRewards": {
Expand All @@ -337,6 +337,15 @@
"deploymentBlock": 35877889,
"deploymentTimestamp": 1758545129175,
"deployed": true
},
"MigratorV6Epochs9to12Rewards": {
"evmAddress": "0x055244C91583ccD32D13624c4e00408f3541A615",
"version": "1.0.0",
"gitBranch": "release/v8-v10-freeze-from-main",
"gitCommitHash": "c05a10cee76b88c08507142e2567cd1d941e27e6",
"deploymentBlock": 45730863,
"deploymentTimestamp": 1778251074980,
"deployed": true
}
}
}
24 changes: 12 additions & 12 deletions deployments/base_sepolia_test_contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,12 @@
"deployed": true
},
"Staking": {
"evmAddress": "0x42b5938C40649CcDdB531349eE9fd1A951193BE5",
"version": "1.0.1",
"gitBranch": "main",
"gitCommitHash": "0a60162a01dace5e6f414efa34d7ac62168349ba",
"deploymentBlock": 28140035,
"deploymentTimestamp": 1752048362857,
"evmAddress": "0x7fFaCf01Afb11Bc04Db81c4D06D3FAcB22a5Db2b",
"version": "1.0.2-freeze",
"gitBranch": "release/v8-v10-freeze-from-main",
"gitCommitHash": "5a977832e411e14b3f86a0cecf8b1d7df298ce9a",
"deploymentBlock": 41239453,
"deploymentTimestamp": 1778247195847,
"deployed": true
},
"Profile": {
Expand Down Expand Up @@ -281,12 +281,12 @@
"deployed": true
},
"RandomSampling": {
"evmAddress": "0x95CF1b97f62F268804F4d82Bf1c98dEEFA8AF5A3",
"version": "1.0.0",
"gitBranch": "RFC-26-update",
"gitCommitHash": "90b2d522e4cc50b106487462ded7fa9e6a767c4b",
"deploymentBlock": 37268602,
"deploymentTimestamp": 1770305493101,
"evmAddress": "0xA2DA5A7C44dCf84106e3Bb3363C8e56E130fDB7b",
"version": "1.0.1-freeze",
"gitBranch": "release/v8-v10-freeze-from-main",
"gitCommitHash": "5a977832e411e14b3f86a0cecf8b1d7df298ce9a",
"deploymentBlock": 41239456,
"deploymentTimestamp": 1778247201718,
"deployed": true
},
"StakingKPI": {
Expand Down
Loading
Loading