Skip to content

Commit

Permalink
sync with jato-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
mtabasco committed Sep 29, 2023
2 parents 47236de + 3da66d3 commit a5c5bc9
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 26 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- [22d2859](https://github.com/bloxapp/ssv-network/pull/262/commits/22d2859d8fe6267b09c7a1c9c645df19bdaa03ff) Fix bug in network earnings withdrawals.
- [d25d188](https://github.com/bloxapp/ssv-network/pull/265/commits/d25d18886459e631fb4453df7a47db19982ec80e) Fix Types.shrink() bug.

### Added
- [bf0c51d](https://github.com/bloxapp/ssv-network/pull/263/commits/bf0c51d4df191018052d11425c9fcc252de61431) A validator can voluntarily exit.


## [Released]

## [v1.0.0.rc4] - 2023-08-31

- Audit fixes/recommendations
- Validate a cluster with 0 validators can not be liquidated
- Deployment process now uses hardhat tasks
- The DAO can set a maximum operator fee (SSV)
- Remove the setRegisterAuth function (register operator/validator without restrictions)
- SSVNetworkViews contract does not throw an error as a way of return.
4 changes: 4 additions & 0 deletions contracts/SSVNetwork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ contract SSVNetwork is
_delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]);
}

function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override {
_delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]);
}

function updateNetworkFee(uint256 fee) external override onlyOwner {
_delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]);
}
Expand Down
7 changes: 7 additions & 0 deletions contracts/interfaces/ISSVClusters.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ interface ISSVClusters is ISSVNetworkCore {
/// @param cluster Cluster where the withdrawal will be made
function withdraw(uint64[] memory operatorIds, uint256 tokenAmount, Cluster memory cluster) external;

/// @notice Fires the exit event for a validator
/// @param publicKey The public key of the validator to be exited
/// @param operatorIds Array of IDs of operators managing the validator
function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external;

/**
* @dev Emitted when the validator has been added.
* @param publicKey The public key of a validator.
Expand All @@ -81,4 +86,6 @@ interface ISSVClusters is ISSVNetworkCore {
event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster);

event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster);

event ValidatorExited(bytes indexed publicKey, uint64[] operatorIds);
}
27 changes: 27 additions & 0 deletions contracts/libraries/ValidatorLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.18;

import "../interfaces/ISSVNetworkCore.sol";
import "./SSVStorage.sol";

library ValidatorLib {
function validateState(
bytes calldata publicKey,
uint64[] calldata operatorIds,
StorageData storage s
) internal view returns (bytes32 hashedValidator) {
hashedValidator = keccak256(abi.encodePacked(publicKey, msg.sender));
bytes32 validatorData = s.validatorPKs[hashedValidator];

if (validatorData == bytes32(0)) {
revert ISSVNetworkCore.ValidatorDoesNotExist();
}
bytes32 mask = ~bytes32(uint256(1)); // All bits set to 1 except LSB

bytes32 hashedOperatorIds = keccak256(abi.encodePacked(operatorIds)) & mask; // Clear LSB of provided operator ids
if ((validatorData & mask) != hashedOperatorIds) {
// Clear LSB of stored validator data and compare
revert ISSVNetworkCore.IncorrectValidatorState();
}
}
}
22 changes: 8 additions & 14 deletions contracts/modules/SSVClusters.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "../libraries/ClusterLib.sol";
import "../libraries/OperatorLib.sol";
import "../libraries/ProtocolLib.sol";
import "../libraries/CoreLib.sol";
import "../libraries/ValidatorLib.sol";
import "../libraries/SSVStorage.sol";
import "../libraries/SSVStorageProtocol.sol";

Expand Down Expand Up @@ -145,20 +146,7 @@ contract SSVClusters is ISSVClusters {
) external override {
StorageData storage s = SSVStorage.load();

bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, msg.sender));

bytes32 mask = ~bytes32(uint256(1)); // All bits set to 1 except LSB
bytes32 validatorData = s.validatorPKs[hashedValidator];

if (validatorData == bytes32(0)) {
revert ValidatorDoesNotExist();
}

bytes32 hashedOperatorIds = keccak256(abi.encodePacked(operatorIds)) & mask; // Clear LSB of provided operator ids
if ((validatorData & mask) != hashedOperatorIds) {
// Clear LSB of stored validator data and compare
revert IncorrectValidatorState();
}
bytes32 hashedValidator = ValidatorLib.validateState(publicKey, operatorIds, s);

bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s);

Expand Down Expand Up @@ -344,4 +332,10 @@ contract SSVClusters is ISSVClusters {

emit ClusterWithdrawn(msg.sender, operatorIds, amount, cluster);
}

function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override {
ValidatorLib.validateState(publicKey, operatorIds, SSVStorage.load());

emit ValidatorExited(publicKey, operatorIds);
}
}
9 changes: 8 additions & 1 deletion contracts/test/SSVNetworkUpgrade.sol
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ contract SSVNetworkUpgrade is
);
}

function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override {
_delegateCall(
SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS],
abi.encodeWithSignature("exitValidator(bytes,uint64[]))", publicKey, operatorIds)
);
}

function updateNetworkFee(uint256 fee) external override onlyOwner {
_delegateCall(
SSVStorage.load().ssvContracts[SSVModules.SSV_DAO],
Expand Down Expand Up @@ -336,7 +343,7 @@ contract SSVNetworkUpgrade is
}

function updateMaximumOperatorFee(uint64 maxFee) external override {
_delegateCall(
_delegateCall(
SSVStorage.load().ssvContracts[SSVModules.SSV_DAO],
abi.encodeWithSignature("updateMaximumOperatorFee(uint64)", maxFee)
);
Expand Down
19 changes: 10 additions & 9 deletions docs/local-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ Execute the steps to set up all tools needed.
Copy [.env.example](../.env.example) to `.env` and edit to suit.
- `[NETWORK]_ETH_NODE_URL` RPC URL of the node
- `[NETWORK]_OWNER_PRIVATE_KEY` Private key of the deployer account, without 0x prefix
- `GAS_PRICE` example 30000000000
- `GAS` example 8000000
- `ETHERSCAN_KEY` etherescan API key to verify deployed contracts
- `SSVTOKEN_ADDRESS` SSV Token contract address to be used in custom networks. Keep it empty to deploy a mocked SSV token.
- `MINIMUM_BLOCKS_BEFORE_LIQUIDATION` a number of blocks before the cluster enters into a liquidatable state. Example: 214800 = 30 days
- `OPERATOR_MAX_FEE_INCREASE` the fee increase limit in percentage with this format: 100% = 10000, 10% = 1000 - using 10000 to represent 2 digit precision
- `DECLARE_OPERATOR_FEE_PERIOD` the period in which an operator can declare a fee change (seconds)
- `EXECUTE_OPERATOR_FEE_PERIOD` the period in which an operator fee change can be executed (seconds)
- `VALIDATORS_PER_OPERATOR_LIMIT` the number of validators an operator can manage
- `GAS_PRICE` Example 30000000000
- `GAS` Example 8000000
- `ETHERSCAN_KEY` Etherescan API key to verify deployed contracts
- `SSV_TOKEN_ADDRESS` SSV Token contract address to be used in custom networks. Keep it empty to deploy a mocked SSV token.
- `MINIMUM_BLOCKS_BEFORE_LIQUIDATION` A number of blocks before the cluster enters into a liquidatable state. Example: 214800 = 30 days
- `OPERATOR_MAX_FEE_INCREASE` The fee increase limit in percentage with this format: 100% = 10000, 10% = 1000 - using 10000 to represent 2 digit precision
- `DECLARE_OPERATOR_FEE_PERIOD` The period in which an operator can declare a fee change (seconds)
- `EXECUTE_OPERATOR_FEE_PERIOD` The period in which an operator fee change can be executed (seconds)
- `VALIDATORS_PER_OPERATOR_LIMIT` The number of validators an operator can manage
- `MINIMUM_LIQUIDATION_COLLATERAL` The lowest number in wei a cluster can have before its liquidatable


#### Network configuration
Expand Down
4 changes: 2 additions & 2 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ POSITIONAL ARGUMENTS:
params Function parameters
Example:
npx hardhat --network goerli_testnet upgrade:proxy --proxyAddress 0x1234... --contract SSVNetworkV2 --initFunction initializev2 --params param1 param2
npx hardhat --network goerli_testnet upgrade:proxy --proxy-address 0x1234... --contract SSVNetworkV2 --init-function initializev2 param1 param2
```

### Update a module
Expand All @@ -100,7 +100,7 @@ OPTIONS:
Example:
Update 'SSVOperators' module contract in the SSVNetwork
npx hardhat --network goerli_testnet update:module --module SSVOperators --attach-module true --proxyAddress 0x1234...
npx hardhat --network goerli_testnet update:module --module SSVOperators --attach-module true --proxy-address 0x1234...
```

### Upgrade a library
Expand Down
3 changes: 3 additions & 0 deletions test/helpers/gas-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum GasGroup {
DEPOSIT,
WITHDRAW_CLUSTER_BALANCE,
WITHDRAW_OPERATOR_BALANCE,
VALIDATOR_EXIT,

LIQUIDATE_CLUSTER_4,
LIQUIDATE_CLUSTER_7,
Expand Down Expand Up @@ -81,6 +82,8 @@ const MAX_GAS_PER_GROUP: any = {
[GasGroup.DEPOSIT]: 77500,
[GasGroup.WITHDRAW_CLUSTER_BALANCE]: 94500,
[GasGroup.WITHDRAW_OPERATOR_BALANCE]: 64900,
[GasGroup.VALIDATOR_EXIT]: 41200,

[GasGroup.LIQUIDATE_CLUSTER_4]: 129300,
[GasGroup.LIQUIDATE_CLUSTER_7]: 170500,
[GasGroup.LIQUIDATE_CLUSTER_10]: 211600,
Expand Down
154 changes: 154 additions & 0 deletions test/validators/others.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Declare imports
import * as helpers from '../helpers/contract-helpers';
import { expect } from 'chai';
import { trackGas, GasGroup } from '../helpers/gas-usage';

// Declare globals
let ssvNetworkContract: any, minDepositAmount: any, firstCluster: any;

describe('Other Validator Tests', () => {
beforeEach(async () => {
// Initialize contract
const metadata = (await helpers.initializeContract());
ssvNetworkContract = metadata.contract;

minDepositAmount = (helpers.CONFIG.minimalBlocksBeforeLiquidation + 10) * helpers.CONFIG.minimalOperatorFee * 4;

// Register operators
await helpers.registerOperators(0, 14, helpers.CONFIG.minimalOperatorFee);

// Register a validator
// cold register
await helpers.coldRegisterValidator();

// first validator
await helpers.DB.ssvToken.connect(helpers.DB.owners[1]).approve(ssvNetworkContract.address, minDepositAmount);
const register = await trackGas(ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator(
helpers.DataGenerator.publicKey(1),
[1, 2, 3, 4],
helpers.DataGenerator.shares(4),
minDepositAmount,
{
validatorCount: 0,
networkFeeIndex: 0,
index: 0,
balance: 0,
active: true
}
), [GasGroup.REGISTER_VALIDATOR_NEW_STATE]);
firstCluster = register.eventsByName.ValidatorAdded[0].args;
});

it('Exiting a validator emits "ValidatorExited"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
firstCluster.operatorIds,
)).to.emit(ssvNetworkContract, 'ValidatorExited')
.withArgs(helpers.DataGenerator.publicKey(1), firstCluster.operatorIds);
});

it('Exiting a validator gas limit', async () => {
await trackGas(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
firstCluster.operatorIds,
), [GasGroup.VALIDATOR_EXIT]);
});

it('Exiting one of the validators in a cluster emits "ValidatorExited"', async () => {
await helpers.DB.ssvToken.connect(helpers.DB.owners[1]).approve(ssvNetworkContract.address, minDepositAmount);
await trackGas(ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator(
helpers.DataGenerator.publicKey(2),
[1, 2, 3, 4],
helpers.DataGenerator.shares(4),
minDepositAmount,
firstCluster.cluster
));

await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(2),
firstCluster.operatorIds,
)).to.emit(ssvNetworkContract, 'ValidatorExited')
.withArgs(helpers.DataGenerator.publicKey(2), firstCluster.operatorIds);
});

it('Exiting a removed validator reverts "ValidatorDoesNotExist"', async () => {
await ssvNetworkContract.connect(helpers.DB.owners[1]).removeValidator(
helpers.DataGenerator.publicKey(1),
firstCluster.operatorIds,
firstCluster.cluster
);

await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
firstCluster.operatorIds
)).to.be.revertedWithCustomError(ssvNetworkContract, 'ValidatorDoesNotExist');
});

it('Exiting a non-existing validator reverts "ValidatorDoesNotExist"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(12),
firstCluster.operatorIds
)).to.be.revertedWithCustomError(ssvNetworkContract, 'ValidatorDoesNotExist');
});

it('Exiting a validator with empty operator list reverts "IncorrectValidatorState"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
[]
)).to.be.revertedWithCustomError(ssvNetworkContract, 'IncorrectValidatorState');
});

it('Exiting a validator with empty public key reverts "ValidatorDoesNotExist"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
'0x',
firstCluster.operatorIds
)).to.be.revertedWithCustomError(ssvNetworkContract, 'ValidatorDoesNotExist');
});

it('Exiting a validator using the wrong account reverts "ValidatorDoesNotExist"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[2]).exitValidator(
helpers.DataGenerator.publicKey(1),
firstCluster.operatorIds
)).to.be.revertedWithCustomError(ssvNetworkContract, 'ValidatorDoesNotExist');
});

it('Exiting a validator with incorrect operators (unsorted list) reverts with "IncorrectValidatorState"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
[4, 3, 2, 1]
)).to.be.revertedWithCustomError(ssvNetworkContract, 'IncorrectValidatorState');
});

it('Exiting a validator with incorrect operators (too many operators) reverts with "IncorrectValidatorState"', async () => {
minDepositAmount = (helpers.CONFIG.minimalBlocksBeforeLiquidation + 10) * helpers.CONFIG.minimalOperatorFee * 13;

await helpers.DB.ssvToken.connect(helpers.DB.owners[2]).approve(ssvNetworkContract.address, minDepositAmount);
const register = await trackGas(ssvNetworkContract.connect(helpers.DB.owners[2]).registerValidator(
helpers.DataGenerator.publicKey(2),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
helpers.DataGenerator.shares(13),
minDepositAmount,
{
validatorCount: 0,
networkFeeIndex: 0,
index: 0,
balance: 0,
active: true
}
));
const secondCluster = register.eventsByName.ValidatorAdded[0].args;

await expect(ssvNetworkContract.connect(helpers.DB.owners[2]).exitValidator(
helpers.DataGenerator.publicKey(2),
secondCluster.operatorIds,
)).to.emit(ssvNetworkContract, 'ValidatorExited')
.withArgs(helpers.DataGenerator.publicKey(2), secondCluster.operatorIds);
});

it('Exiting a validator with incorrect operators reverts with "IncorrectValidatorState"', async () => {
await expect(ssvNetworkContract.connect(helpers.DB.owners[1]).exitValidator(
helpers.DataGenerator.publicKey(1),
[1, 2, 3, 5]
)).to.be.revertedWithCustomError(ssvNetworkContract, 'IncorrectValidatorState');
});
});

0 comments on commit a5c5bc9

Please sign in to comment.