diff --git a/abi/Profile.json b/abi/Profile.json index 5826a464..c206bd10 100644 --- a/abi/Profile.json +++ b/abi/Profile.json @@ -356,6 +356,19 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [], + "name": "nodeOperatorFeesStorage", + "outputs": [ + { + "internalType": "contract NodeOperatorFeesStorage", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "parametersStorage", diff --git a/contracts/v1/Profile.sol b/contracts/v1/Profile.sol index 1ecffd3f..cc89da4a 100644 --- a/contracts/v1/Profile.sol +++ b/contracts/v1/Profile.sol @@ -10,6 +10,7 @@ import {ParametersStorage} from "./storage/ParametersStorage.sol"; import {ProfileStorage} from "./storage/ProfileStorage.sol"; import {StakingStorage} from "./storage/StakingStorage.sol"; import {Staking} from "./Staking.sol"; +import {NodeOperatorFeesStorage} from "../v2/storage/NodeOperatorFeesStorage.sol"; import {WhitelistStorage} from "./storage/WhitelistStorage.sol"; import {ContractStatus} from "./abstract/ContractStatus.sol"; import {Initializable} from "./interface/Initializable.sol"; @@ -33,7 +34,7 @@ contract Profile is Named, Versioned, ContractStatus, Initializable { event AskUpdated(uint72 indexed identityId, bytes nodeId, uint96 ask); string private constant _NAME = "Profile"; - string private constant _VERSION = "1.1.1"; + string private constant _VERSION = "1.1.2"; HashingProxy public hashingProxy; Identity public identityContract; @@ -43,6 +44,7 @@ contract Profile is Named, Versioned, ContractStatus, Initializable { ParametersStorage public parametersStorage; ProfileStorage public profileStorage; WhitelistStorage public whitelistStorage; + NodeOperatorFeesStorage public nodeOperatorFeesStorage; // solhint-disable-next-line no-empty-blocks constructor(address hubAddress) ContractStatus(hubAddress) {} @@ -76,6 +78,7 @@ contract Profile is Named, Versioned, ContractStatus, Initializable { parametersStorage = ParametersStorage(hub.getContractAddress("ParametersStorage")); profileStorage = ProfileStorage(hub.getContractAddress("ProfileStorage")); whitelistStorage = WhitelistStorage(hub.getContractAddress("WhitelistStorage")); + nodeOperatorFeesStorage = NodeOperatorFeesStorage(hub.getContractAddress("NodeOperatorFeesStorage")); } function name() external pure virtual override returns (string memory) { @@ -96,6 +99,7 @@ contract Profile is Named, Versioned, ContractStatus, Initializable { ) external onlyWhitelisted { IdentityStorage ids = identityStorage; ProfileStorage ps = profileStorage; + NodeOperatorFeesStorage nofs = nodeOperatorFeesStorage; Identity id = identityContract; if (ids.getIdentityId(msg.sender) != 0) { @@ -136,7 +140,7 @@ contract Profile is Named, Versioned, ContractStatus, Initializable { ps.createProfile(identityId, nodeId, address(sharesContract)); _setAvailableNodeAddresses(identityId); - stakingStorage.setOperatorFee(identityId, initialOperatorFee); + nofs.addOperatorFee(identityId, initialOperatorFee, uint248(block.timestamp)); emit ProfileCreated(identityId, nodeId, adminWallet, address(sharesContract), initialOperatorFee); } diff --git a/contracts/v2/storage/NodeOperatorFeesStorage.sol b/contracts/v2/storage/NodeOperatorFeesStorage.sol index b42e10d0..566f88cd 100644 --- a/contracts/v2/storage/NodeOperatorFeesStorage.sol +++ b/contracts/v2/storage/NodeOperatorFeesStorage.sol @@ -10,7 +10,7 @@ import {NodeOperatorStructs} from "../structs/NodeOperatorStructs.sol"; contract NodeOperatorFeesStorage is Named, Versioned, HubDependent { string private constant _NAME = "NodeOperatorFeesStorage"; - string private constant _VERSION = "2.0.0"; + string private constant _VERSION = "2.0.2"; bool private _delayFreePeriodSet; uint256 public delayFreePeriodEnd; @@ -19,7 +19,6 @@ contract NodeOperatorFeesStorage is Named, Versioned, HubDependent { // identityId => OperatorFee[] mapping(uint72 => NodeOperatorStructs.OperatorFee[]) public operatorFees; - // solhint-disable-next-line no-empty-blocks constructor(address hubAddress, uint256 migrationPeriodEnd_) HubDependent(hubAddress) { migrationPeriodEnd = migrationPeriodEnd_; } @@ -43,12 +42,11 @@ contract NodeOperatorFeesStorage is Named, Versioned, HubDependent { return _VERSION; } - function migrateOldOperatorFees( - NodeOperatorStructs.OperatorFees[] memory legacyFees - ) external onlyHubOwner timeLimited { + function migrateOldOperatorFees(NodeOperatorStructs.OperatorFees[] memory legacyFees) external timeLimited { for (uint i; i < legacyFees.length; ) { - operatorFees[legacyFees[i].identityId] = legacyFees[i].fees; + require(operatorFees[legacyFees[i].identityId].length == 0); + operatorFees[legacyFees[i].identityId] = legacyFees[i].fees; unchecked { i++; } diff --git a/deploy/028_deploy_node_operator_fees_storage.ts b/deploy/028_deploy_node_operator_fees_storage.ts index b10b82fa..f2d38329 100644 --- a/deploy/028_deploy_node_operator_fees_storage.ts +++ b/deploy/028_deploy_node_operator_fees_storage.ts @@ -1,12 +1,12 @@ +import { BigNumberish } from 'ethers'; import { DeployFunction } from 'hardhat-deploy/types'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { ShardingTableStructsV1 } from '../typechain/contracts/v2/ShardingTable.sol/ShardingTableV2'; - const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const isDeployed = hre.helpers.isDeployed('NodeOperatorFeesStorage'); + const isMigration = hre.helpers.contractDeployments.contracts['NodeOperatorFeesStorage']?.migration || false; - if (isDeployed) { + if (isDeployed && !isMigration) { return; } @@ -15,22 +15,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployer } = await hre.getNamedAccounts(); - let shardingTableABI; - let shardingTableAddress; - if (Object.keys(hre.helpers.contractDeployments.contracts).includes('OldShardingTable')) { - shardingTableABI = hre.helpers.getAbi('ShardingTable'); - shardingTableAddress = hre.helpers.contractDeployments.contracts['OldShardingTable'].evmAddress; - delete hre.helpers.contractDeployments.contracts['OldShardingTable']; - console.log(`Found V1 ShardingTable address: ${shardingTableAddress}`); - } else { - shardingTableABI = hre.helpers.getAbi('ShardingTableV2'); - shardingTableAddress = hre.helpers.contractDeployments.contracts['ShardingTable'].evmAddress; - console.log(`Found V2 ShardingTable address: ${shardingTableAddress}`); - } - const ShardingTable = await hre.ethers.getContractAt(shardingTableABI, shardingTableAddress, deployer); - - const stakingStorageAddress = hre.helpers.contractDeployments.contracts['StakingStorage'].evmAddress; - const StakingStorage = await hre.ethers.getContractAt('StakingStorage', stakingStorageAddress, deployer); + const oldNodeOperatorFeesStorageAddress = + hre.helpers.contractDeployments.contracts['NodeOperatorFeesStorage'].evmAddress; + const OldNodeOperatorFeesStorage = await hre.ethers.getContractAt( + 'NodeOperatorFeesStorage', + oldNodeOperatorFeesStorageAddress, + ); const nofcsAddress = hre.helpers.contractDeployments.contracts['NodeOperatorFeeChangesStorage']?.evmAddress; let nofcs = null; @@ -39,43 +29,133 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { nofcs = await hre.ethers.getContractAt(abi, nofcsAddress, deployer); } - console.log(`Getting list of nodes in Sharding Table...`); - const nodes: ShardingTableStructsV1.NodeInfoStructOutput[] = await ShardingTable['getShardingTable()'](); - const identityIds = nodes.map((node) => node.identityId); + const stakingStorageAddress = hre.helpers.contractDeployments.contracts['StakingStorage'].evmAddress; + const StakingStorage = await hre.ethers.getContractAt('StakingStorage', stakingStorageAddress, deployer); - console.log(`Starting migration of the old operator fees...`); - for (const identityId of identityIds) { + const storageLayout = { + astId: 1238, + contract: 'IdentityStorageFlattened.sol:IdentityStorage', + label: '_identityId', + offset: 20, + slot: 0, + type: 't_uint72', + }; + const storageVariableType = { + t_uint72: { + encoding: 'inplace', + label: 'uint72', + numberOfBytes: 9, + }, + }; + + console.log('Getting current next identityId from IdentityStorage...'); + const storageSlot = await hre.ethers.provider.getStorageAt( + hre.helpers.contractDeployments.contracts['IdentityStorage'].evmAddress, + 0, + ); + const variableSlot = storageSlot.slice( + storageSlot.length - + 2 * + (storageLayout.offset + + storageVariableType[storageLayout.type as keyof typeof storageVariableType].numberOfBytes), + storageSlot.length - storageLayout.offset * 2, + ); + console.log(`Storage slot ${storageLayout.slot}: ${storageSlot}`); + console.log(`Variable slot: ${variableSlot}`); + const nextIdentityId = parseInt(variableSlot, 16); + console.log(`Current next identityId: ${nextIdentityId}`); + + console.log(`Starting migration of the old operator fees... Latest identityId: ${nextIdentityId - 1}`); + for (let identityId = 1; identityId < nextIdentityId; identityId++) { console.log(`--------------------------------------------------------`); console.log(`IdentityId: ${identityId}`); - const operatorFees = []; + let operatorFees: { feePercentage: BigNumberish; effectiveDate: number }[] = []; - const activeOperatorFeePercentage = await StakingStorage.operatorFees(identityId); + const oldContractOperatorFees = await OldNodeOperatorFeesStorage.getOperatorFees(identityId); - console.log(`Active operatorFee in the StakingStorage: ${activeOperatorFeePercentage.toString()}%`); + console.log(`Old operatorFees in the old NodeOperatorFeesStorage: ${JSON.stringify(oldContractOperatorFees)}`); - if (!activeOperatorFeePercentage.eq(0)) { - operatorFees.push({ - feePercentage: activeOperatorFeePercentage, - effectiveDate: timestampNow, + if (oldContractOperatorFees.length != 0) { + const fees = oldContractOperatorFees.map((x: BigNumberish[]) => { + return { feePercentage: x[0], effectiveDate: Number(x[1].toString()) }; }); + + if (hre.network.name.startsWith('gnosis')) { + operatorFees = operatorFees.concat(fees); + } else { + oldOperatorFees.push({ + identityId, + fees, + }); + continue; + } + } + + let stakingStorageSource = false; + if (operatorFees.length == 0) { + const activeOperatorFeePercentage = await StakingStorage.operatorFees(identityId); + + console.log(`Active operatorFee in the StakingStorage: ${activeOperatorFeePercentage.toString()}%`); + + if (!activeOperatorFeePercentage.eq(0)) { + operatorFees.push({ + feePercentage: activeOperatorFeePercentage, + effectiveDate: timestampNow, + }); + stakingStorageSource = true; + } } if (nofcs !== null) { const pendingOperatorFee = await nofcs.operatorFeeChangeRequests(identityId); + if (Number(pendingOperatorFee.timestamp.toString() == 0)) { + if (operatorFees.length > 0) { + oldOperatorFees.push({ + identityId, + fees: operatorFees, + }); + } else { + oldOperatorFees.push({ + identityId, + fees: [{ feePercentage: 0, effectiveDate: timestampNow }], + }); + } + continue; + } + console.log(`Pending operatorFee in the NodeOperatorFeeChangesStorage: ${pendingOperatorFee.newFee.toString()}%`); - if (!pendingOperatorFee.timestamp.eq(0)) { - if (pendingOperatorFee.timestamp < operatorFees[0].effectiveDate) { - operatorFees[0].effectiveDate = pendingOperatorFee.timestamp - 1; - } + const exists = operatorFees.some( + (obj) => + Number(obj.feePercentage.toString()) === Number(pendingOperatorFee.newFee.toString()) && + Number(obj.effectiveDate.toString()) === Number(pendingOperatorFee.timestamp.toString()), + ); - operatorFees.push({ - feePercentage: pendingOperatorFee.newFee, - effectiveDate: pendingOperatorFee.timestamp, + if (exists) { + console.log(`Pending operatorFee is already a part of the fees array from old NodeOperatorFeesStorage`); + oldOperatorFees.push({ + identityId, + fees: operatorFees, }); + continue; + } + + if ( + (stakingStorageSource && + Number(pendingOperatorFee.timestamp.toString()) < Number(operatorFees[0].effectiveDate.toString())) || + (operatorFees.length == 1 && Number(operatorFees[0].effectiveDate.toString()) == 1716291685) + ) { + operatorFees[0].effectiveDate = Number(pendingOperatorFee.timestamp.toString()) - 1; } + + operatorFees.push({ + feePercentage: pendingOperatorFee.newFee, + effectiveDate: Number(pendingOperatorFee.timestamp.toString()), + }); + + operatorFees.sort((a, b) => a.effectiveDate - b.effectiveDate); } console.log(`--------------------------------------------------------`); @@ -85,6 +165,11 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { identityId, fees: operatorFees, }); + } else { + oldOperatorFees.push({ + identityId, + fees: [{ feePercentage: 0, effectiveDate: timestampNow }], + }); } } @@ -98,38 +183,24 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }); const chunkSize = 10; - const encodedDataArray: string[] = oldOperatorFees.reduce((acc, _, currentIndex, array) => { - if (currentIndex % chunkSize === 0) { - // Encode and push the function data for a slice of the array - acc.push( - NodeOperatorFeesStorage.interface.encodeFunctionData('migrateOldOperatorFees', [ - array.slice(currentIndex, currentIndex + chunkSize), - ]), - ); - } - return acc; - }, []); - - if (hre.network.config.environment === 'development') { - const { deployer } = await hre.getNamedAccounts(); - - const hubControllerAddress = hre.helpers.contractDeployments.contracts['HubController'].evmAddress; - const HubController = await hre.ethers.getContractAt('HubController', hubControllerAddress, deployer); - - for (let i = 0; i < encodedDataArray.length; i++) { - const migrateOldOperatorFeesTx = await HubController.forwardCall( - NodeOperatorFeesStorage.address, - encodedDataArray[i], - ); - await migrateOldOperatorFeesTx.wait(); - } - } else { - for (let i = 0; i < encodedDataArray.length; i++) { - hre.helpers.setParametersEncodedData.push(['NodeOperatorFeesStorage', [encodedDataArray[i]]]); - } + const totalChunks = Math.ceil(oldOperatorFees.length / chunkSize); + + console.log(`Starting migration of operator fees for ${oldOperatorFees.length} nodes...`); + for (let i = 0; i < oldOperatorFees.length; i += chunkSize) { + const chunk = oldOperatorFees.slice(i, i + chunkSize); + const chunkNumber = Math.floor(i / chunkSize) + 1; + const percentageDone = ((chunkNumber / totalChunks) * 100).toFixed(2); + console.log( + `Processing chunk ${chunkNumber} out of ${totalChunks} (starting at index ${i}):`, + JSON.stringify(chunk), + ); + console.log(`Percentage done: ${percentageDone}%`); + + const tx = await NodeOperatorFeesStorage.migrateOldOperatorFees(chunk); + await tx.wait(); } }; export default func; func.tags = ['NodeOperatorFeesStorage', 'v2']; -func.dependencies = ['HubV2', 'StakingStorage', 'ShardingTableV2']; +func.dependencies = ['HubV2', 'ContentAssetStorageV2', 'StakingStorage', 'ShardingTableV2']; diff --git a/deployments/gnosis_mainnet_contracts.json b/deployments/gnosis_mainnet_contracts.json index db20c6b9..16e8d99a 100644 --- a/deployments/gnosis_mainnet_contracts.json +++ b/deployments/gnosis_mainnet_contracts.json @@ -210,12 +210,12 @@ "deployed": true }, "Profile": { - "evmAddress": "0x5deb369B8C0930853cD192d85f1389151e009CF4", - "version": "1.1.1", - "gitBranch": "main", - "gitCommitHash": "26881f1a79eaf41f13d1c48a2c5952d0a1d14274", - "deploymentBlock": 32446897, - "deploymentTimestamp": 1707908838740, + "evmAddress": "0x5a1114614b7790849B38198A289FA8d41C42cc24", + "version": "1.1.2", + "gitBranch": "operatorFee-fixes", + "gitCommitHash": "63e8ce3dfa370844af8318182890ba23976f7daf", + "deploymentBlock": 34057405, + "deploymentTimestamp": 1716290847473, "deployed": true }, "CommitManagerV1": { @@ -273,12 +273,12 @@ "deployed": true }, "NodeOperatorFeesStorage": { - "evmAddress": "0x5adAC1b63407C490aC0Fb80E85CE7014101c8610", - "version": "2.0.0", - "gitBranch": "mainnet-deployment/4.2.7", - "gitCommitHash": "8839a0e5c92a413b06d20fa15335950bb625f890", - "deploymentBlock": 33619390, - "deploymentTimestamp": 1714031946358, + "evmAddress": "0x2F2031A35dE9297f2372569551F8682E818C4943", + "version": "2.0.2", + "gitBranch": "operatorFee-fixes-2", + "gitCommitHash": "38d7bc4595f2ff59bf0d3d11efd291241ecc992f", + "deploymentBlock": 34206991, + "deploymentTimestamp": 1717062353134, "deployed": true } } diff --git a/deployments/otp_mainnet_contracts.json b/deployments/otp_mainnet_contracts.json index f8a8b700..1fa4778a 100644 --- a/deployments/otp_mainnet_contracts.json +++ b/deployments/otp_mainnet_contracts.json @@ -68,13 +68,13 @@ "deployed": true }, "Profile": { - "evmAddress": "0x8453855ED7a42d98F0abCbEDBD61ac082c5337B5", - "substrateAddress": "5EMjsczngyeEYRQVCDxeLthH85Upcsiw3G663SnzQsPuNpc9", - "version": "1.1.1", - "gitBranch": "main", - "gitCommitHash": "cdd09025ee035eb72c4322639cefe1848dd90ecb", - "deploymentBlock": 4786001, - "deploymentTimestamp": 1713972267333, + "evmAddress": "0xDfB94C5021B1684A83001734c3cFd733415d9CcD", + "substrateAddress": "5EMjsd171Acgqe4PrafYj2BStycTBUqfSs2xX1QYGE1f3Ju6", + "version": "1.1.2", + "gitBranch": "operatorFee-fixes", + "gitCommitHash": "63e8ce3dfa370844af8318182890ba23976f7daf", + "deploymentBlock": 4973043, + "deploymentTimestamp": 1716277322852, "deployed": true }, "ProfileStorage": { @@ -197,13 +197,13 @@ "deployed": true }, "NodeOperatorFeesStorage": { - "evmAddress": "0x030d086a005F2e5AB99D7a0a73c0a7C92028640b", - "substrateAddress": "5EMjsczLnayCy2aw6rNrJh76pxZqZgtfSban41RzmYmNzfgo", - "version": "2.0.0", - "gitBranch": "main", - "gitCommitHash": "cdd09025ee035eb72c4322639cefe1848dd90ecb", - "deploymentBlock": 4785998, - "deploymentTimestamp": 1713972219931, + "evmAddress": "0xe761209C8598a933b0F2CE3BdA11a9850Ca320BA", + "substrateAddress": "5EMjsd18Y8u6kqeznzaNcWNy4XpuX6TJTqvxsPRstGK1FMJb", + "version": "2.0.2", + "gitBranch": "operatorFee-fixes-2", + "gitCommitHash": "38d7bc4595f2ff59bf0d3d11efd291241ecc992f", + "deploymentBlock": 5035511, + "deploymentTimestamp": 1717062664410, "deployed": true }, "Staking": {