Skip to content

Commit

Permalink
Added ProfileV2 with sharding table addition of profile creation, ada…
Browse files Browse the repository at this point in the history
…pted deployment scripts, redeployed contracts on V8 testnet
  • Loading branch information
br41nl3t committed Oct 4, 2024
1 parent 055075f commit b283bc0
Show file tree
Hide file tree
Showing 16 changed files with 1,021 additions and 42 deletions.
638 changes: 638 additions & 0 deletions abi/ProfileV2.json

Large diffs are not rendered by default.

296 changes: 296 additions & 0 deletions contracts/v2/Profile.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.16;

import {HashingProxy} from "../v1/HashingProxy.sol";
import {Identity} from "../v1/Identity.sol";
import {Shares} from "../v1/Shares.sol";
import {IdentityStorageV2} from "./storage/IdentityStorage.sol";
import {ParametersStorage} from "../v1/storage/ParametersStorage.sol";
import {ProfileStorage} from "../v1/storage/ProfileStorage.sol";
import {ShardingTableV2} from "./ShardingTable.sol";
import {StakingStorage} from "../v1/storage/StakingStorage.sol";
import {StakingV2} from "./Staking.sol";
import {NodeOperatorFeesStorage} from "./storage/NodeOperatorFeesStorage.sol";
import {WhitelistStorage} from "../v1/storage/WhitelistStorage.sol";
import {ContractStatus} from "../v1/abstract/ContractStatus.sol";
import {Initializable} from "../v1/interface/Initializable.sol";
import {Named} from "../v1/interface/Named.sol";
import {Versioned} from "../v1/interface/Versioned.sol";
import {GeneralErrors} from "../v1/errors/GeneralErrors.sol";
import {ProfileErrors} from "../v1/errors/ProfileErrors.sol";
import {StakingErrors} from "../v1/errors/StakingErrors.sol";
import {UnorderedIndexableContractDynamicSetLib} from "../v1/utils/UnorderedIndexableContractDynamicSet.sol";
import {ADMIN_KEY, OPERATIONAL_KEY} from "../v1/constants/IdentityConstants.sol";

contract ProfileV2 is Named, Versioned, ContractStatus, Initializable {
event ProfileCreated(
uint72 indexed identityId,
bytes nodeId,
address adminWallet,
address sharesContractAddress,
uint8 initialOperatorFee
);
event ProfileDeleted(uint72 indexed identityId);
event AskUpdated(uint72 indexed identityId, bytes nodeId, uint96 ask);
event AccumulatedOperatorFeeWithdrawalStarted(
uint72 indexed identityId,
bytes nodeId,
uint96 oldAccumulatedOperatorFee,
uint96 newAccumulatedOperatorFee,
uint256 withdrawalPeriodEnd
);
event AccumulatedOperatorFeeWithdrawn(uint72 indexed identityId, bytes nodeId, uint96 withdrawnAmount);
event AccumulatedOperatorFeeRestaked(
uint72 indexed identityId,
bytes nodeId,
uint96 oldAccumulatedOperatorFee,
uint96 newAccumulatedOperatorFee
);

string private constant _NAME = "ProfileV2";
string private constant _VERSION = "2.0.0";

HashingProxy public hashingProxy;
Identity public identityContract;
ShardingTableV2 public shardingTableContract;
StakingStorage public stakingStorage;
StakingV2 public stakingContract;
IdentityStorageV2 public identityStorage;
ParametersStorage public parametersStorage;
ProfileStorage public profileStorage;
WhitelistStorage public whitelistStorage;
NodeOperatorFeesStorage public nodeOperatorFeesStorage;

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

modifier onlyIdentityOwner(uint72 identityId) {
_checkIdentityOwner(identityId);
_;
}

modifier onlyAdmin(uint72 identityId) {
_checkAdmin(identityId);
_;
}

modifier onlyOperational(uint72 identityId) {
_checkOperational(identityId);
_;
}

modifier onlyWhitelisted() {
_checkWhitelist();
_;
}

function initialize() public onlyHubOwner {
hashingProxy = HashingProxy(hub.getContractAddress("HashingProxy"));
identityContract = Identity(hub.getContractAddress("Identity"));
shardingTableContract = ShardingTableV2(hub.getContractAddress("ShardingTable"));
stakingStorage = StakingStorage(hub.getContractAddress("StakingStorage"));
stakingContract = StakingV2(hub.getContractAddress("Staking"));
identityStorage = IdentityStorageV2(hub.getContractAddress("IdentityStorage"));
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) {
return _NAME;
}

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

function createProfile(
address adminWallet,
address[] calldata operationalWallets,
bytes calldata nodeId,
string calldata sharesTokenName,
string calldata sharesTokenSymbol,
uint8 initialOperatorFee
) external onlyWhitelisted {
IdentityStorageV2 ids = identityStorage;
ProfileStorage ps = profileStorage;
NodeOperatorFeesStorage nofs = nodeOperatorFeesStorage;
Identity id = identityContract;

if (ids.getIdentityId(msg.sender) != 0) {
revert ProfileErrors.IdentityAlreadyExists(ids.getIdentityId(msg.sender), msg.sender);
}
if (operationalWallets.length > parametersStorage.opWalletsLimitOnProfileCreation()) {
revert ProfileErrors.TooManyOperationalWallets(
parametersStorage.opWalletsLimitOnProfileCreation(),
uint16(operationalWallets.length)
);
}
if (nodeId.length == 0) {
revert ProfileErrors.EmptyNodeId();
}
if (ps.nodeIdsList(nodeId)) {
revert ProfileErrors.NodeIdAlreadyExists(nodeId);
}
if (keccak256(abi.encodePacked(sharesTokenName)) == keccak256(abi.encodePacked(""))) {
revert ProfileErrors.EmptySharesTokenName();
}
if (keccak256(abi.encodePacked(sharesTokenSymbol)) == keccak256(abi.encodePacked(""))) {
revert ProfileErrors.EmptySharesTokenSymbol();
}
if (ps.sharesNames(sharesTokenName)) {
revert ProfileErrors.SharesTokenNameAlreadyExists(sharesTokenName);
}
if (ps.sharesSymbols(sharesTokenSymbol)) {
revert ProfileErrors.SharesTokenSymbolAlreadyExists(sharesTokenSymbol);
}
if (initialOperatorFee > 100) {
revert ProfileErrors.OperatorFeeOutOfRange(initialOperatorFee);
}
uint72 identityId = id.createIdentity(msg.sender, adminWallet);
id.addOperationalWallets(identityId, operationalWallets);

Shares sharesContract = new Shares(address(hub), sharesTokenName, sharesTokenSymbol);

ps.createProfile(identityId, nodeId, address(sharesContract));
_setAvailableNodeAddresses(identityId);

nofs.addOperatorFee(identityId, initialOperatorFee, uint248(block.timestamp));

shardingTableContract.insertNode(identityId);

emit ProfileCreated(identityId, nodeId, adminWallet, address(sharesContract), initialOperatorFee);
}

function setAsk(uint72 identityId, uint96 ask) external onlyIdentityOwner(identityId) {
if (ask == 0) {
revert ProfileErrors.ZeroAsk();
}
ProfileStorage ps = profileStorage;
ps.setAsk(identityId, ask);

emit AskUpdated(identityId, ps.getNodeId(identityId), ask);
}

function _setAvailableNodeAddresses(uint72 identityId) internal virtual {
ProfileStorage ps = profileStorage;
HashingProxy hp = hashingProxy;

bytes memory nodeId = ps.getNodeId(identityId);
bytes32 nodeAddress;

UnorderedIndexableContractDynamicSetLib.Contract[] memory hashFunctions = hp.getAllHashFunctions();
require(hashFunctions.length <= parametersStorage.hashFunctionsLimit(), "Too many hash functions!");
uint8 hashFunctionId;
for (uint8 i; i < hashFunctions.length; ) {
hashFunctionId = hashFunctions[i].id;
nodeAddress = hp.callHashFunction(hashFunctionId, nodeId);
ps.setNodeAddress(identityId, hashFunctionId, nodeAddress);
unchecked {
i++;
}
}
}

function stakeAccumulatedOperatorFee(uint72 identityId, uint96 restakeAmount) external onlyAdmin(identityId) {
require(restakeAmount != 0, "Restake amount cannot be 0");

ProfileStorage ps = profileStorage;

uint96 oldAccumulatedOperatorFee = ps.getAccumulatedOperatorFee(identityId);

require(restakeAmount <= oldAccumulatedOperatorFee, "Restake must be <= balance");

ps.setAccumulatedOperatorFee(identityId, oldAccumulatedOperatorFee - restakeAmount);
stakingContract.addStake(msg.sender, identityId, restakeAmount);

emit AccumulatedOperatorFeeRestaked(
identityId,
ps.getNodeId(identityId),
oldAccumulatedOperatorFee,
oldAccumulatedOperatorFee - restakeAmount
);
}

function startAccumulatedOperatorFeeWithdrawal(
uint72 identityId,
uint96 withdrawalAmount
) external onlyAdmin(identityId) {
require(withdrawalAmount != 0, "Withdrawal amount cannot be 0");

ProfileStorage ps = profileStorage;

uint96 oldAccumulatedOperatorFee = ps.getAccumulatedOperatorFee(identityId);

require(withdrawalAmount <= oldAccumulatedOperatorFee, "Withdrawal must be <= balance");

ps.setAccumulatedOperatorFee(identityId, oldAccumulatedOperatorFee - withdrawalAmount);
ps.setAccumulatedOperatorFeeWithdrawalAmount(
identityId,
ps.getAccumulatedOperatorFeeWithdrawalAmount(identityId) + withdrawalAmount
);
ps.setAccumulatedOperatorFeeWithdrawalTimestamp(
identityId,
block.timestamp + parametersStorage.stakeWithdrawalDelay()
);

emit AccumulatedOperatorFeeWithdrawalStarted(
identityId,
ps.getNodeId(identityId),
oldAccumulatedOperatorFee,
oldAccumulatedOperatorFee - withdrawalAmount,
block.timestamp + parametersStorage.stakeWithdrawalDelay()
);
}

function withdrawAccumulatedOperatorFee(uint72 identityId) external onlyAdmin(identityId) {
ProfileStorage ps = profileStorage;

uint96 withdrawalAmount = ps.getAccumulatedOperatorFeeWithdrawalAmount(identityId);

if (withdrawalAmount == 0) {
revert StakingErrors.WithdrawalWasntInitiated();
}
if (ps.getAccumulatedOperatorFeeWithdrawalTimestamp(identityId) >= block.timestamp) {
revert StakingErrors.WithdrawalPeriodPending(
block.timestamp,
ps.getAccumulatedOperatorFeeWithdrawalTimestamp(identityId)
);
}
ps.setAccumulatedOperatorFeeWithdrawalAmount(identityId, 0);
ps.setAccumulatedOperatorFeeWithdrawalTimestamp(identityId, 0);
ps.transferAccumulatedOperatorFee(msg.sender, withdrawalAmount);

emit AccumulatedOperatorFeeWithdrawn(identityId, ps.getNodeId(identityId), withdrawalAmount);
}

function _checkIdentityOwner(uint72 identityId) internal view virtual {
if (
!identityStorage.keyHasPurpose(identityId, keccak256(abi.encodePacked(msg.sender)), ADMIN_KEY) &&
!identityStorage.keyHasPurpose(identityId, keccak256(abi.encodePacked(msg.sender)), OPERATIONAL_KEY)
) {
revert GeneralErrors.OnlyProfileAdminOrOperationalAddressesFunction(msg.sender);
}
}

function _checkAdmin(uint72 identityId) internal view virtual {
if (!identityStorage.keyHasPurpose(identityId, keccak256(abi.encodePacked(msg.sender)), ADMIN_KEY)) {
revert GeneralErrors.OnlyProfileAdminFunction(msg.sender);
}
}

function _checkOperational(uint72 identityId) internal view virtual {
if (!identityStorage.keyHasPurpose(identityId, keccak256(abi.encodePacked(msg.sender)), OPERATIONAL_KEY)) {
revert GeneralErrors.OnlyProfileOperationalWalletFunction(msg.sender);
}
}

function _checkWhitelist() internal view virtual {
WhitelistStorage ws = whitelistStorage;
if (ws.whitelistingEnabled() && !ws.whitelisted(msg.sender)) {
revert GeneralErrors.OnlyWhitelistedAddressesFunction(msg.sender);
}
}
}
35 changes: 35 additions & 0 deletions deploy/035_deploy_profile_v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
if (
hre.helpers.isDeployed('Profile') &&
(hre.helpers.contractDeployments.contracts['Profile'].version === undefined ||
hre.helpers.contractDeployments.contracts['Profile'].version?.startsWith('1.'))
) {
return;
}

console.log('Deploying Profile V2...');

await hre.helpers.deploy({
newContractName: 'ProfileV2',
newContractNameInHub: 'Profile',
});
};

export default func;
func.tags = ['Profile', 'v2'];
func.dependencies = [
'Hub',
'Identity',
'IdentityStorageV2',
'ParametersStorage',
'ProfileStorage',
'HashingProxy',
'SHA256',
'ShardingTableV2',
'Staking',
'WhitelistStorage',
'NodeOperatorFeesStorage',
];
12 changes: 11 additions & 1 deletion deploy/035_deploy_profile.ts → deploy/036_deploy_profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
if (
hre.helpers.isDeployed('Profile') &&
(hre.helpers.contractDeployments.contracts['Profile'].version === undefined ||
hre.helpers.contractDeployments.contracts['Profile'].version?.startsWith('2.'))
) {
return;
}

console.log('Deploying Profile V1...');

await hre.helpers.deploy({
newContractName: 'Profile',
});
Expand All @@ -17,7 +27,7 @@ func.dependencies = [
'ProfileStorage',
'HashingProxy',
'SHA256',
'Staking',
'StakingV2',
'WhitelistStorage',
'NodeOperatorFeesStorage',
];
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit b283bc0

Please sign in to comment.