diff --git a/scripts/deploy-registry.template.ts b/scripts/deploy-registry.template.ts index 2d1e8f5c4..c4b14f164 100644 --- a/scripts/deploy-registry.template.ts +++ b/scripts/deploy-registry.template.ts @@ -31,14 +31,36 @@ export async function deploy() { const managerFacet = await deployContractWithDeployer( deployer, 'SubnetActorManagerFacet', - { - LibStaking: LIBMAP['LibStaking'], - }, + {}, txArgs, ) const managerSelectors = getSelectors(managerFacet) // console.log("manager address:", managerFacet.address); + const pauserFacet = await deployContractWithDeployer( + deployer, + 'SubnetActorPauseFacet', + {}, + txArgs, + ) + const pauserSelectors = getSelectors(pauserFacet) + + const rewarderFacet = await deployContractWithDeployer( + deployer, + 'SubnetActorRewardFacet', + { LibStaking: LIBMAP['LibStaking'] }, + txArgs, + ) + const rewarderSelectors = getSelectors(rewarderFacet) + + const checkpointerFacet = await deployContractWithDeployer( + deployer, + 'SubnetActorCheckpointingFacet', + {}, + txArgs, + ) + const checkpointerSelectors = getSelectors(checkpointerFacet) + //deploy subnet registry diamond const registry = await ethers.getContractFactory('SubnetRegistryDiamond', { signer: deployer, @@ -48,8 +70,14 @@ export async function deploy() { gateway: gatewayAddress, getterFacet: getterFacet.address, managerFacet: managerFacet.address, + rewarderFacet: rewarderFacet.address, + pauserFacet: pauserFacet.address, + checkpointerFacet: checkpointerFacet.address, subnetGetterSelectors: getterSelectors, subnetManagerSelectors: managerSelectors, + checkpointerSelectors: checkpointerSelectors, + pauserSelectors: pauserSelectors, + rewarderSelectors: rewarderSelectors, } const facetCuts = [] //TODO diff --git a/scripts/deploy-sa-diamond.ts b/scripts/deploy-sa-diamond.ts index 9bdbaef06..0cc5fa439 100644 --- a/scripts/deploy-sa-diamond.ts +++ b/scripts/deploy-sa-diamond.ts @@ -26,15 +26,22 @@ async function deploySubnetActorDiamond( SubnetIDHelper: libs['SubnetIDHelper'], } - const managerFacetLibs: Libraries = { - LibStaking: libs['LibStaking'], - } + const managerFacetLibs: Libraries = {} + + const rewarderFacetLibs: Libraries = {} + + const pauserFacetLibs: Libraries = {} + + const checkpointerFacetLibs: Libraries = {} const facets = [ { name: 'DiamondLoupeFacet', libs: {} }, { name: 'DiamondCutFacet', libs: {} }, { name: 'SubnetActorGetterFacet', libs: getterFacetLibs }, { name: 'SubnetActorManagerFacet', libs: managerFacetLibs }, + { name: 'SubnetActorRewardFacet', libs: rewarderFacetLibs }, + { name: 'SubnetActorCheckpointingFacet', libs: checkpointerFacetLibs }, + { name: 'SubnetActorPauseFacet', libs: pauserFacetLibs }, ] // The `facetCuts` variable is the FacetCut[] that contains the functions to add during diamond deployment const facetCuts = [] diff --git a/scripts/python/build_selector_library.py b/scripts/python/build_selector_library.py index 04a22b2e6..2cb4785db 100644 --- a/scripts/python/build_selector_library.py +++ b/scripts/python/build_selector_library.py @@ -81,6 +81,9 @@ def main(): 'src/gateway/router/XnetMessagingFacet.sol', 'src/subnet/SubnetActorGetterFacet.sol', 'src/subnet/SubnetActorManagerFacet.sol', + 'src/subnet/SubnetActorPauseFacet.sol', + 'src/subnet/SubnetActorRewardFacet.sol', + 'src/subnet/SubnetActorCheckpointingFacet.sol', 'src/subnetregistry/RegisterSubnetFacet.sol', 'src/subnetregistry/SubnetGetterFacet.sol', 'test/helpers/ERC20PresetFixedSupply.sol', @@ -88,7 +91,7 @@ def main(): 'test/helpers/NumberContractFacetSeven.sol', 'test/helpers/SelectorLibrary.sol', 'test/helpers/TestUtils.sol', - 'test/mocks/SubnetActorManagerFacetMock.sol', + 'test/mocks/SubnetActorMock.sol', ] for filepath in filepaths_to_target: diff --git a/src/gateway/router/BottomUpRouterFacet.sol b/src/gateway/router/BottomUpRouterFacet.sol index 6e47ecbdf..f48523685 100644 --- a/src/gateway/router/BottomUpRouterFacet.sol +++ b/src/gateway/router/BottomUpRouterFacet.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity 0.8.19; -import {ISubnetActor} from "../../interfaces/ISubnetActor.sol"; +import {IRelayerRewardDistributor} from "../../interfaces/ISubnetActor.sol"; import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol"; import {BottomUpMsgBatch} from "../../structs/CrossNet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; @@ -73,7 +73,7 @@ contract BottomUpRouterFacet is GatewayActorModifiers { Address.functionCallWithValue({ target: msg.sender, data: abi.encodeCall( - ISubnetActor.distributeRewardToRelayers, + IRelayerRewardDistributor.distributeRewardToRelayers, (block.number, totalFee, QuorumObjKind.Checkpoint) ), value: totalFee diff --git a/src/gateway/router/CheckpointingFacet.sol b/src/gateway/router/CheckpointingFacet.sol index 15ef47979..ab37e51c7 100644 --- a/src/gateway/router/CheckpointingFacet.sol +++ b/src/gateway/router/CheckpointingFacet.sol @@ -8,7 +8,7 @@ import {LibQuorum} from "../../lib/LibQuorum.sol"; import {Subnet} from "../../structs/Subnet.sol"; import {QuorumObjKind} from "../../structs/Quorum.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; -import {ISubnetActor} from "../../interfaces/ISubnetActor.sol"; +import {IRelayerRewardDistributor} from "../../interfaces/ISubnetActor.sol"; import {InvalidBatchSource, NotEnoughBalance, MaxMsgsPerBatchExceeded, BatchWithNoMessages, InvalidCheckpointSource, InvalidCrossMsgNonce, InvalidCrossMsgDstSubnet, CheckpointAlreadyExists} from "../../errors/IPCErrors.sol"; import {NotRegisteredSubnet, SubnetNotActive, SubnetNotFound, InvalidSubnet, CheckpointNotCreated} from "../../errors/IPCErrors.sol"; @@ -42,7 +42,7 @@ contract CheckpointingFacet is GatewayActorModifiers { Address.functionCallWithValue({ target: msg.sender, data: abi.encodeCall( - ISubnetActor.distributeRewardToRelayers, + IRelayerRewardDistributor.distributeRewardToRelayers, (checkpoint.blockHeight, 0, QuorumObjKind.Checkpoint) ), value: 0 diff --git a/src/interfaces/ISubnetActor.sol b/src/interfaces/ISubnetActor.sol index 5f155aa41..b1f236236 100644 --- a/src/interfaces/ISubnetActor.sol +++ b/src/interfaces/ISubnetActor.sol @@ -4,47 +4,7 @@ pragma solidity 0.8.19; import {BottomUpCheckpoint} from "../structs/CrossNet.sol"; import {QuorumObjKind} from "../structs/Quorum.sol"; -/// @title Subnet Actor interface -/// @author LimeChain team -interface ISubnetActor { - /// Called by peers looking to join a subnet. - /// - /// It implements the basic logic to onboard new peers to the subnet. - function join(bytes calldata metadata) external payable; - - /// Called by peers looking to leave a subnet. - function leave() external; - - /// Method that allows a validator to increase their stake - function stake() external payable; - - /// Method that allows to pre-fund an address in the subnet before it bootstraps. - function preFund() external payable; - - /// Method that allows to recover initial balance for an address from a subnet that hasn't bootstrapped yet. - function preRelease(uint256 amount) external; - - /// Method that allows a validator to unstake their collateral from a subnet - function unstake(uint256 amount) external; - - /// Unregister the subnet from the hierarchy, making it no longer discoverable. - function kill() external; - - /// Validator claims released collateral - function claim() external; - - /// Relayer claims a reward - function claimRewardForRelayer() external; - - /// Executes the checkpoint if it is valid. - /// It triggers the commitment of the checkpoint, the execution of related cross-net messages, - /// and any other side-effects that need to be triggered by the checkpoint such as relayer reward book keeping. - function submitCheckpoint( - BottomUpCheckpoint calldata checkpoint, - address[] calldata signatories, - bytes[] calldata signatures - ) external; - +interface IRelayerRewardDistributor { /// reward the relayers for processing checkpoint at height `height`. /// The reword includes the fixed reward for a relayer defined in the contract and `amount` of fees from the cross-messages. function distributeRewardToRelayers(uint256 height, uint256 amount, QuorumObjKind kind) external payable; diff --git a/src/lib/LibPausable.sol b/src/lib/LibPausable.sol index 78a73c93b..3e880ee20 100644 --- a/src/lib/LibPausable.sol +++ b/src/lib/LibPausable.sol @@ -16,7 +16,7 @@ abstract contract Pausable { event Paused(address account); /** - * @dev Emitted when the pause is lifted by `account`. + * @dev Emitted when the unpause is triggered by `account`. */ event Unpaused(address account); @@ -42,11 +42,23 @@ abstract contract Pausable { _; } + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + /** * @dev Throws if the contract is paused. */ - function _requireNotPaused() internal view virtual { - if (paused()) { + function _requireNotPaused() internal view { + if (_paused()) { revert EnforcedPause(); } } @@ -54,14 +66,14 @@ abstract contract Pausable { /** * @dev Throws if the contract is not paused. */ - function _requirePaused() internal view virtual { - if (!paused()) { + function _requirePaused() internal view { + if (!_paused()) { revert ExpectedPause(); } } - /// @notice sets if to pause the contract - function paused() public view returns(bool) { + /// @notice returns true if the contract is paused + function _paused() internal view returns(bool) { PausableStorage storage s = pausableStorage(); return s.paused; } @@ -73,10 +85,11 @@ abstract contract Pausable { * * - The contract must not be paused. */ - function _pause() internal whenNotPaused { + function _pause() internal { + _requireNotPaused(); PausableStorage storage s = pausableStorage(); s.paused = true; - emit Unpaused(msg.sender); + emit Paused(msg.sender); } /** @@ -86,7 +99,7 @@ abstract contract Pausable { * * - The contract must be paused. */ - function _unpause() internal { + function _unpause() internal { _requirePaused(); PausableStorage storage s = pausableStorage(); s.paused = false; diff --git a/src/subnet/SubnetActorCheckpointingFacet.sol b/src/subnet/SubnetActorCheckpointingFacet.sol new file mode 100644 index 000000000..5f6da4472 --- /dev/null +++ b/src/subnet/SubnetActorCheckpointingFacet.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.19; + +import {InvalidBatchEpoch, MaxMsgsPerBatchExceeded, BatchWithNoMessages, InvalidSignatureErr, InvalidCheckpointEpoch} from "../errors/IPCErrors.sol"; +import {IGateway} from "../interfaces/IGateway.sol"; +import {BottomUpCheckpoint, BottomUpMsgBatch, BottomUpMsgBatchInfo} from "../structs/CrossNet.sol"; +import {Validator, ValidatorSet} from "../structs/Subnet.sol"; +import {MultisignatureChecker} from "../lib/LibMultisignatureChecker.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {SubnetActorModifiers} from "../lib/LibSubnetActorStorage.sol"; +import {LibValidatorSet, LibStaking} from "../lib/LibStaking.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; +import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; +import {Pausable} from "../lib/LibPausable.sol"; + +contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { + using EnumerableSet for EnumerableSet.AddressSet; + using LibValidatorSet for ValidatorSet; + + /// @notice Submits a checkpoint commitment for execution. + /// @dev It triggers the commitment of the checkpoint and any other side-effects that + /// need to be triggered by the checkpoint such as relayer reward book keeping. + /// @param checkpoint The executed bottom-up checkpoint. + /// @param signatories The addresses of validators signing the checkpoint. + /// @param signatures The signatures of validators on the checkpoint. + function submitCheckpoint( + BottomUpCheckpoint calldata checkpoint, + address[] calldata signatories, + bytes[] calldata signatures + ) external whenNotPaused { + // the checkpoint height must be equal to the last bottom-up checkpoint height or + // the next one + if ( + checkpoint.blockHeight != s.lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod && + checkpoint.blockHeight != s.lastBottomUpCheckpointHeight + ) { + revert InvalidCheckpointEpoch(); + } + bytes32 checkpointHash = keccak256(abi.encode(checkpoint)); + + if (checkpoint.blockHeight == s.lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod) { + // validate signatures and quorum threshold, revert if validation fails + validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures}); + + // If the checkpoint height is the next expected height then this is a new checkpoint which must be executed + // in the Gateway Actor, the checkpoint and the relayer must be stored, last bottom-up checkpoint updated. + s.committedCheckpoints[checkpoint.blockHeight] = checkpoint; + + // slither-disable-next-line unused-return + s.relayerRewards.checkpointRewarded[checkpoint.blockHeight].add(msg.sender); + + s.lastBottomUpCheckpointHeight = checkpoint.blockHeight; + + // Commit in gateway to distribute rewards + IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); + + // confirming the changes in membership in the child + LibStaking.confirmChange(checkpoint.nextConfigurationNumber); + } else if (checkpoint.blockHeight == s.lastBottomUpCheckpointHeight) { + // If the checkpoint height is equal to the last checkpoint height, then this is a repeated submission. + // We should store the relayer, but not to execute checkpoint again. + // In this case, we do not verify the signatures for this checkpoint again, + // but we add the relayer to the list of all relayers for this checkpoint to be rewarded later. + // The reason for comparing hashes instead of verifying signatures is the following: + // once the checkpoint is executed, the active validator set changes + // and can only be used to validate the next checkpoint, not another instance of the last one. + bytes32 lastCheckpointHash = keccak256(abi.encode(s.committedCheckpoints[checkpoint.blockHeight])); + if (checkpointHash == lastCheckpointHash) { + // slither-disable-next-line unused-return + s.relayerRewards.checkpointRewarded[checkpoint.blockHeight].add(msg.sender); + } + } + } + + /// @notice Submits a batch of bottom-up messages for execution. + /// @dev It triggers the execution of a cross-net message batch. + /// @param batch The batch of bottom-up messages. + /// @param signatories The addresses of validators signing the batch. + /// @param signatures The signatures of validators on the batch. + function submitBottomUpMsgBatch( + BottomUpMsgBatch calldata batch, + address[] calldata signatories, + bytes[] calldata signatures + ) external { + // forbid the submission of batches from the past + if (batch.blockHeight < s.lastBottomUpBatch.blockHeight) { + revert InvalidBatchEpoch(); + } + if (batch.msgs.length > s.maxMsgsPerBottomUpBatch) { + revert MaxMsgsPerBatchExceeded(); + } + // if the batch height is not max, we only supoprt batch submission in period epochs + if (batch.msgs.length != s.maxMsgsPerBottomUpBatch && batch.blockHeight % s.bottomUpMsgBatchPeriod != 0) { + revert InvalidBatchEpoch(); + } + if (batch.msgs.length == 0) { + revert BatchWithNoMessages(); + } + + bytes32 batchHash = keccak256(abi.encode(batch)); + + if (batch.blockHeight == s.lastBottomUpBatch.blockHeight) { + // If the batch info is equal to the last batch info, then this is a repeated submission. + // We should store the relayer, but not to execute batch again following the same reward logic + // used for checkpoints. + if (batchHash == s.lastBottomUpBatch.hash) { + // slither-disable-next-line unused-return + s.relayerRewards.batchRewarded[batch.blockHeight].add(msg.sender); + } + } else { + // validate signatures and quorum threshold, revert if validation fails + validateActiveQuorumSignatures({signatories: signatories, hash: batchHash, signatures: signatures}); + + // If the checkpoint height is the next expected height then this is a new batch, + // and should be forwarded to the gateway for execution. + s.lastBottomUpBatch = BottomUpMsgBatchInfo({blockHeight: batch.blockHeight, hash: batchHash}); + + // slither-disable-next-line unused-return + s.relayerRewards.batchRewarded[batch.blockHeight].add(msg.sender); + + // Execute messages. + IGateway(s.ipcGatewayAddr).execBottomUpMsgBatch(batch); + } + } + + /// @notice Checks whether the signatures are valid for the provided signatories and hash within the current validator set. + /// Reverts otherwise. + /// @dev Signatories in `signatories` and their signatures in `signatures` must be provided in the same order. + /// Having it public allows external users to perform sanity-check verification if needed. + /// @param signatories The addresses of the signatories. + /// @param hash The hash of the checkpoint. + /// @param signatures The packed signatures of the checkpoint. + function validateActiveQuorumSignatures( + address[] memory signatories, + bytes32 hash, + bytes[] memory signatures + ) public view { + // This call reverts if at least one of the signatories (validator) is not in the active validator set. + uint256[] memory collaterals = s.validatorSet.getTotalPowerOfValidators(signatories); + uint256 activeCollateral = s.validatorSet.getTotalActivePower(); + + uint256 threshold = (activeCollateral * s.majorityPercentage) / 100; + + (bool valid, MultisignatureChecker.Error err) = MultisignatureChecker.isValidWeightedMultiSignature({ + signatories: signatories, + weights: collaterals, + threshold: threshold, + hash: hash, + signatures: signatures + }); + + if (!valid) { + revert InvalidSignatureErr(uint8(err)); + } + } +} diff --git a/src/subnet/SubnetActorManagerFacet.sol b/src/subnet/SubnetActorManagerFacet.sol index 7a6dae3e2..e079d00fb 100644 --- a/src/subnet/SubnetActorManagerFacet.sol +++ b/src/subnet/SubnetActorManagerFacet.sol @@ -2,150 +2,24 @@ pragma solidity 0.8.19; import {VALIDATOR_SECP256K1_PUBLIC_KEY_LENGTH} from "../constants/Constants.sol"; -import {ERR_PERMISSIONED_AND_BOOTSTRAPPED, ERR_VALIDATOR_JOINED, ERR_VALIDATOR_NOT_JOINED} from "../errors/IPCErrors.sol"; -import {InvalidBatchEpoch, MaxMsgsPerBatchExceeded, BatchWithNoMessages, InvalidFederationPayload, SubnetAlreadyBootstrapped, NotEnoughFunds, CollateralIsZero, CannotReleaseZero, NotOwnerOfPublicKey, EmptyAddress, NotEnoughBalance, NotEnoughCollateral, NotValidator, NotAllValidatorsHaveLeft, NotStakedBefore, InvalidSignatureErr, InvalidCheckpointEpoch, InvalidPublicKeyLength, MethodNotAllowed} from "../errors/IPCErrors.sol"; +import {ERR_VALIDATOR_JOINED, ERR_VALIDATOR_NOT_JOINED} from "../errors/IPCErrors.sol"; +import {InvalidFederationPayload, SubnetAlreadyBootstrapped, NotEnoughFunds, CollateralIsZero, CannotReleaseZero, NotOwnerOfPublicKey, EmptyAddress, NotEnoughBalance, NotEnoughCollateral, NotValidator, NotAllValidatorsHaveLeft, InvalidPublicKeyLength, MethodNotAllowed} from "../errors/IPCErrors.sol"; import {IGateway} from "../interfaces/IGateway.sol"; -import {ISubnetActor} from "../interfaces/ISubnetActor.sol"; -import {QuorumObjKind} from "../structs/Quorum.sol"; -import {BottomUpCheckpoint, BottomUpMsgBatch, BottomUpMsgBatchInfo} from "../structs/CrossNet.sol"; -import {Validator, ValidatorSet, PermissionMode} from "../structs/Subnet.sol"; -import {Pausable} from "../lib/LibPausable.sol"; +import {Validator, ValidatorSet} from "../structs/Subnet.sol"; import {LibDiamond} from "../lib/LibDiamond.sol"; -import {MultisignatureChecker} from "../lib/LibMultisignatureChecker.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; import {SubnetActorModifiers} from "../lib/LibSubnetActorStorage.sol"; import {LibValidatorSet, LibStaking} from "../lib/LibStaking.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; +import {Pausable} from "../lib/LibPausable.sol"; -contract SubnetActorManagerFacet is ISubnetActor, SubnetActorModifiers, Pausable, ReentrancyGuard { +contract SubnetActorManagerFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using EnumerableSet for EnumerableSet.AddressSet; using LibValidatorSet for ValidatorSet; using Address for address payable; - event BottomUpCheckpointSubmitted(BottomUpCheckpoint checkpoint, address submitter); - event BottomUpCheckpointExecuted(uint256 epoch, address submitter); - event NextBottomUpCheckpointExecuted(uint256 epoch, address submitter); - - /// @notice Pauses all contract functions with the `whenNotPaused` modifier. - function pause() external { - LibDiamond.enforceIsContractOwner(); - _pause(); - } - - /// @notice Unpauses all contract functions with the `whenNotPaused` modifier. - function unpause() external { - LibDiamond.enforceIsContractOwner(); - _unpause(); - } - - /// @notice Submits a checkpoint commitment for execution. - /// @dev It triggers the commitment of the checkpoint and any other side-effects that - /// need to be triggered by the checkpoint such as relayer reward book keeping. - /// @param checkpoint The executed bottom-up checkpoint. - /// @param signatories The addresses of validators signing the checkpoint. - /// @param signatures The signatures of validators on the checkpoint. - function submitCheckpoint( - BottomUpCheckpoint calldata checkpoint, - address[] calldata signatories, - bytes[] calldata signatures - ) external whenNotPaused { - // the checkpoint height must be equal to the last bottom-up checkpoint height or - // the next one - if ( - checkpoint.blockHeight != s.lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod && - checkpoint.blockHeight != s.lastBottomUpCheckpointHeight - ) { - revert InvalidCheckpointEpoch(); - } - bytes32 checkpointHash = keccak256(abi.encode(checkpoint)); - - if (checkpoint.blockHeight == s.lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod) { - // validate signatures and quorum threshold, revert if validation fails - validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures}); - - // If the checkpoint height is the next expected height then this is a new checkpoint which must be executed - // in the Gateway Actor, the checkpoint and the relayer must be stored, last bottom-up checkpoint updated. - s.committedCheckpoints[checkpoint.blockHeight] = checkpoint; - - // slither-disable-next-line unused-return - s.relayerRewards.checkpointRewarded[checkpoint.blockHeight].add(msg.sender); - - s.lastBottomUpCheckpointHeight = checkpoint.blockHeight; - - // Commit in gateway to distribute rewards - IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); - - // confirming the changes in membership in the child - LibStaking.confirmChange(checkpoint.nextConfigurationNumber); - } else if (checkpoint.blockHeight == s.lastBottomUpCheckpointHeight) { - // If the checkpoint height is equal to the last checkpoint height, then this is a repeated submission. - // We should store the relayer, but not to execute checkpoint again. - // In this case, we do not verify the signatures for this checkpoint again, - // but we add the relayer to the list of all relayers for this checkpoint to be rewarded later. - // The reason for comparing hashes instead of verifying signatures is the following: - // once the checkpoint is executed, the active validator set changes - // and can only be used to validate the next checkpoint, not another instance of the last one. - bytes32 lastCheckpointHash = keccak256(abi.encode(s.committedCheckpoints[checkpoint.blockHeight])); - if (checkpointHash == lastCheckpointHash) { - // slither-disable-next-line unused-return - s.relayerRewards.checkpointRewarded[checkpoint.blockHeight].add(msg.sender); - } - } - } - - /// @notice Submits a batch of bottom-up messages for execution. - /// @dev It triggers the execution of a cross-net message batch. - /// @param batch The batch of bottom-up messages. - /// @param signatories The addresses of validators signing the batch. - /// @param signatures The signatures of validators on the batch. - function submitBottomUpMsgBatch( - BottomUpMsgBatch calldata batch, - address[] calldata signatories, - bytes[] calldata signatures - ) external { - // forbid the submission of batches from the past - if (batch.blockHeight < s.lastBottomUpBatch.blockHeight) { - revert InvalidBatchEpoch(); - } - if (batch.msgs.length > s.maxMsgsPerBottomUpBatch) { - revert MaxMsgsPerBatchExceeded(); - } - // if the batch height is not max, we only supoprt batch submission in period epochs - if (batch.msgs.length != s.maxMsgsPerBottomUpBatch && batch.blockHeight % s.bottomUpMsgBatchPeriod != 0) { - revert InvalidBatchEpoch(); - } - if (batch.msgs.length == 0) { - revert BatchWithNoMessages(); - } - - bytes32 batchHash = keccak256(abi.encode(batch)); - - if (batch.blockHeight == s.lastBottomUpBatch.blockHeight) { - // If the batch info is equal to the last batch info, then this is a repeated submission. - // We should store the relayer, but not to execute batch again following the same reward logic - // used for checkpoints. - if (batchHash == s.lastBottomUpBatch.hash) { - // slither-disable-next-line unused-return - s.relayerRewards.batchRewarded[batch.blockHeight].add(msg.sender); - } - } else { - // validate signatures and quorum threshold, revert if validation fails - validateActiveQuorumSignatures({signatories: signatories, hash: batchHash, signatures: signatures}); - - // If the checkpoint height is the next expected height then this is a new batch, - // and should be forwarded to the gateway for execution. - s.lastBottomUpBatch = BottomUpMsgBatchInfo({blockHeight: batch.blockHeight, hash: batchHash}); - - // slither-disable-next-line unused-return - s.relayerRewards.batchRewarded[batch.blockHeight].add(msg.sender); - - // Execute messages. - IGateway(s.ipcGatewayAddr).execBottomUpMsgBatch(batch); - } - } - /// @notice method to add some initial balance into a subnet that hasn't yet bootstrapped. /// @dev This balance is added to user addresses in genesis, and becomes part of the genesis /// circulating supply. @@ -216,9 +90,17 @@ contract SubnetActorManagerFacet is ISubnetActor, SubnetActorModifiers, Pausable } if (s.bootstrapped) { - LibSubnetActor.postBootstrapSetFederatedPower(validators, publicKeys, powers); + LibSubnetActor.postBootstrapSetFederatedPower({ + validators: validators, + publicKeys: publicKeys, + powers: powers + }); } else { - LibSubnetActor.preBootstrapSetFederatedPower(validators, publicKeys, powers); + LibSubnetActor.preBootstrapSetFederatedPower({ + validators: validators, + publicKeys: publicKeys, + powers: powers + }); } } @@ -371,16 +253,6 @@ contract SubnetActorManagerFacet is ISubnetActor, SubnetActorModifiers, Pausable IGateway(s.ipcGatewayAddr).kill(); } - /// @notice Validator claims their released collateral. - function claim() external nonReentrant whenNotPaused { - LibStaking.claimCollateral(msg.sender); - } - - /// @notice Relayer claims its reward. - function claimRewardForRelayer() external nonReentrant whenNotPaused { - LibStaking.claimRewardForRelayer(msg.sender); - } - /// @notice Add a bootstrap node. /// @param netAddress The network address of the new bootstrap node. function addBootstrapNode(string memory netAddress) external whenNotPaused { @@ -394,82 +266,4 @@ contract SubnetActorManagerFacet is ISubnetActor, SubnetActorModifiers, Pausable // slither-disable-next-line unused-return s.bootstrapOwners.add(msg.sender); } - - /// @notice Distributes rewards to relayers for a specific checkpoint. - /// @param height The height of the checkpoint for which rewards are being distributed. - /// @param reward The total amount of reward to be distributed. - /// @param kind The type of object for which rewards are being distributed. - function distributeRewardToRelayers( - uint256 height, - uint256 reward, - QuorumObjKind kind - ) external payable whenNotPaused onlyGateway { - if (reward == 0) { - return; - } - - // get rewarded addresses - address[] memory relayers = new address[](0); - if (kind == QuorumObjKind.Checkpoint) { - relayers = LibSubnetActor.checkpointRewardedAddrs(height); - } else if (kind == QuorumObjKind.BottomUpMsgBatch) { - // FIXME: The distribution of rewards for batches can't be done - // as for checkpoints (due to how they are submitted). As - // we are running out of time, we'll defer this for the future. - revert MethodNotAllowed("rewards not defined for batches"); - } else { - revert MethodNotAllowed("rewards not defined for object kind"); - } - - // comupte reward - // we are not distributing equally, this logic should be decoupled - // into different reward policies. - uint256 relayersLength = relayers.length; - if (relayersLength == 0) { - return; - } - if (reward < relayersLength) { - return; - } - uint256 relayerReward = reward / relayersLength; - - // distribute reward - for (uint256 i; i < relayersLength; ) { - s.relayerRewards.rewards[relayers[i]] += relayerReward; - unchecked { - ++i; - } - } - } - - /// @notice Checks whether the signatures are valid for the provided signatories and hash within the current validator set. - /// Reverts otherwise. - /// @dev Signatories in `signatories` and their signatures in `signatures` must be provided in the same order. - /// Having it public allows external users to perform sanity-check verification if needed. - /// @param signatories The addresses of the signatories. - /// @param hash The hash of the checkpoint. - /// @param signatures The packed signatures of the checkpoint. - function validateActiveQuorumSignatures( - address[] memory signatories, - bytes32 hash, - bytes[] memory signatures - ) public view { - // This call reverts if at least one of the signatories (validator) is not in the active validator set. - uint256[] memory collaterals = s.validatorSet.getTotalPowerOfValidators(signatories); - uint256 activeCollateral = s.validatorSet.getTotalActivePower(); - - uint256 threshold = (activeCollateral * s.majorityPercentage) / 100; - - (bool valid, MultisignatureChecker.Error err) = MultisignatureChecker.isValidWeightedMultiSignature({ - signatories: signatories, - weights: collaterals, - threshold: threshold, - hash: hash, - signatures: signatures - }); - - if (!valid) { - revert InvalidSignatureErr(uint8(err)); - } - } } diff --git a/src/subnet/SubnetActorPauseFacet.sol b/src/subnet/SubnetActorPauseFacet.sol new file mode 100644 index 000000000..fbfb48ec1 --- /dev/null +++ b/src/subnet/SubnetActorPauseFacet.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.19; + +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {Pausable} from "../lib/LibPausable.sol"; + +contract SubnetActorPauseFacet is Pausable { + /// @notice Pauses all contract functions with the `whenNotPaused` modifier. + function pause() external { + LibDiamond.enforceIsContractOwner(); + _pause(); + } + + /// @notice Unpauses all contract functions with the `whenNotPaused` modifier. + function unpause() external { + LibDiamond.enforceIsContractOwner(); + _unpause(); + } + + /// @notice Returns true if the SubnetActor contract is paused. + function paused() external view returns (bool) { + return _paused(); + } +} diff --git a/src/subnet/SubnetActorRewardFacet.sol b/src/subnet/SubnetActorRewardFacet.sol new file mode 100644 index 000000000..cedfce1d4 --- /dev/null +++ b/src/subnet/SubnetActorRewardFacet.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.19; + +import {MethodNotAllowed} from "../errors/IPCErrors.sol"; +import {IRelayerRewardDistributor} from "../interfaces/ISubnetActor.sol"; +import {QuorumObjKind} from "../structs/Quorum.sol"; +import {Pausable} from "../lib/LibPausable.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {SubnetActorModifiers} from "../lib/LibSubnetActorStorage.sol"; +import {LibStaking} from "../lib/LibStaking.sol"; +import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; + +contract SubnetActorRewardFacet is IRelayerRewardDistributor, SubnetActorModifiers, ReentrancyGuard, Pausable { + /// @notice Validator claims their released collateral. + function claim() external nonReentrant whenNotPaused { + LibStaking.claimCollateral(msg.sender); + } + + /// @notice Relayer claims its reward. + function claimRewardForRelayer() external nonReentrant whenNotPaused { + LibStaking.claimRewardForRelayer(msg.sender); + } + + /// @notice Distributes rewards to relayers for a specific checkpoint. + /// @param height The height of the checkpoint for which rewards are being distributed. + /// @param reward The total amount of reward to be distributed. + /// @param kind The type of object for which rewards are being distributed. + function distributeRewardToRelayers( + uint256 height, + uint256 reward, + QuorumObjKind kind + ) external payable whenNotPaused onlyGateway { + if (reward == 0) { + return; + } + + // get rewarded addresses + address[] memory relayers = new address[](0); + if (kind == QuorumObjKind.Checkpoint) { + relayers = LibSubnetActor.checkpointRewardedAddrs(height); + } else if (kind == QuorumObjKind.BottomUpMsgBatch) { + // FIXME: The distribution of rewards for batches can't be done + // as for checkpoints (due to how they are submitted). As + // we are running out of time, we'll defer this for the future. + revert MethodNotAllowed("rewards not defined for batches"); + } else { + revert MethodNotAllowed("rewards not defined for object kind"); + } + + // comupte reward + // we are not distributing equally, this logic should be decoupled + // into different reward policies. + uint256 relayersLength = relayers.length; + if (relayersLength == 0) { + return; + } + if (reward < relayersLength) { + return; + } + uint256 relayerReward = reward / relayersLength; + + // distribute reward + for (uint256 i; i < relayersLength; ) { + s.relayerRewards.rewards[relayers[i]] += relayerReward; + unchecked { + ++i; + } + } + } +} diff --git a/test/IntegrationTestBase.sol b/test/IntegrationTestBase.sol index f2c5b376d..91f9780c4 100644 --- a/test/IntegrationTestBase.sol +++ b/test/IntegrationTestBase.sol @@ -27,12 +27,17 @@ import {XnetMessagingFacet} from "../src/gateway/router/XnetMessagingFacet.sol"; import {TopDownFinalityFacet} from "../src/gateway/router/TopDownFinalityFacet.sol"; import {BottomUpRouterFacet} from "../src/gateway/router/BottomUpRouterFacet.sol"; -import {SubnetActorManagerFacetMock} from "./mocks/SubnetActorManagerFacetMock.sol"; +import {SubnetActorMock} from "./mocks/SubnetActorMock.sol"; import {SubnetActorManagerFacet} from "../src/subnet/SubnetActorManagerFacet.sol"; +import {SubnetActorPauseFacet} from "../src/subnet/SubnetActorPauseFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../src/subnet/SubnetActorCheckpointingFacet.sol"; +import {SubnetActorRewardFacet} from "../src/subnet/SubnetActorRewardFacet.sol"; import {SubnetActorGetterFacet} from "../src/subnet/SubnetActorGetterFacet.sol"; + import {SubnetRegistryDiamond} from "../src/SubnetRegistryDiamond.sol"; import {RegisterSubnetFacet} from "../src/subnetregistry/RegisterSubnetFacet.sol"; import {SubnetGetterFacet} from "../src/subnetregistry/SubnetGetterFacet.sol"; + import {DiamondLoupeFacet} from "../src/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../src/diamond/DiamondCutFacet.sol"; import {SupplySourceHelper} from "../src/lib/SupplySourceHelper.sol"; @@ -137,21 +142,31 @@ contract TestGatewayActor is Test, TestParams { contract TestSubnetActor is Test, TestParams { bytes4[] saGetterSelectors; bytes4[] saManagerSelectors; + bytes4[] saPauserSelectors; + bytes4[] saRewarderSelectors; + bytes4[] saCheckpointerSelectors; bytes4[] saManagerMockedSelectors; bytes4[] saCutterSelectors; bytes4[] saLouperSelectors; SubnetActorDiamond saDiamond; SubnetActorManagerFacet saManager; - SubnetActorManagerFacetMock saMockedManager; + SubnetActorMock saMock; SubnetActorGetterFacet saGetter; + SubnetActorRewardFacet saRewarder; + SubnetActorPauseFacet saPauser; + SubnetActorCheckpointingFacet saCheckpointer; + DiamondCutFacet saCutter; DiamondLoupeFacet saLouper; constructor() { saGetterSelectors = SelectorLibrary.resolveSelectors("SubnetActorGetterFacet"); saManagerSelectors = SelectorLibrary.resolveSelectors("SubnetActorManagerFacet"); - saManagerMockedSelectors = SelectorLibrary.resolveSelectors("SubnetActorManagerFacetMock"); + saPauserSelectors = SelectorLibrary.resolveSelectors("SubnetActorPauseFacet"); + saRewarderSelectors = SelectorLibrary.resolveSelectors("SubnetActorRewardFacet"); + saCheckpointerSelectors = SelectorLibrary.resolveSelectors("SubnetActorCheckpointingFacet"); + saManagerMockedSelectors = SelectorLibrary.resolveSelectors("SubnetActorMock"); saCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); saLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); } @@ -216,6 +231,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, saDiamond = createSubnetActor(saConstructorParams); saManager = SubnetActorManagerFacet(address(saDiamond)); saGetter = SubnetActorGetterFacet(address(saDiamond)); + saPauser = SubnetActorPauseFacet(address(saDiamond)); + saRewarder = SubnetActorRewardFacet(address(saDiamond)); + saCheckpointer = SubnetActorCheckpointingFacet(address(saDiamond)); saLouper = DiamondLoupeFacet(address(saDiamond)); saCutter = DiamondCutFacet(address(saDiamond)); @@ -315,14 +333,17 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function createSubnetActorDiamondWithFaucets( SubnetActorDiamond.ConstructorParams memory params, - address getterFaucet, - address managerFaucet + address getter, + address manager, + address pauser, + address rewarder, + address checkpointer ) public returns (SubnetActorDiamond) { - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](2); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](5); diamondCut[0] = ( IDiamond.FacetCut({ - facetAddress: getterFaucet, + facetAddress: getter, action: IDiamond.FacetCutAction.Add, functionSelectors: saGetterSelectors }) @@ -330,12 +351,36 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, diamondCut[1] = ( IDiamond.FacetCut({ - facetAddress: managerFaucet, + facetAddress: manager, action: IDiamond.FacetCutAction.Add, functionSelectors: saManagerSelectors }) ); + diamondCut[2] = ( + IDiamond.FacetCut({ + facetAddress: pauser, + action: IDiamond.FacetCutAction.Add, + functionSelectors: saPauserSelectors + }) + ); + + diamondCut[3] = ( + IDiamond.FacetCut({ + facetAddress: rewarder, + action: IDiamond.FacetCutAction.Add, + functionSelectors: saRewarderSelectors + }) + ); + + diamondCut[4] = ( + IDiamond.FacetCut({ + facetAddress: checkpointer, + action: IDiamond.FacetCutAction.Add, + functionSelectors: saCheckpointerSelectors + }) + ); + saDiamond = new SubnetActorDiamond(diamondCut, params); return saDiamond; } @@ -343,10 +388,14 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function createSubnetActor(SubnetActorDiamond.ConstructorParams memory params) public returns (SubnetActorDiamond) { SubnetActorManagerFacet manager = new SubnetActorManagerFacet(); SubnetActorGetterFacet getter = new SubnetActorGetterFacet(); + SubnetActorPauseFacet pauser = new SubnetActorPauseFacet(); + SubnetActorRewardFacet rewarder = new SubnetActorRewardFacet(); + SubnetActorCheckpointingFacet checkpointer = new SubnetActorCheckpointingFacet(); + DiamondLoupeFacet louper = new DiamondLoupeFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](4); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](7); diamondCut[0] = ( IDiamond.FacetCut({ @@ -366,17 +415,41 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, diamondCut[2] = ( IDiamond.FacetCut({ - facetAddress: address(cutter), + facetAddress: address(pauser), action: IDiamond.FacetCutAction.Add, - functionSelectors: gwCutterSelectors + functionSelectors: saPauserSelectors }) ); diamondCut[3] = ( + IDiamond.FacetCut({ + facetAddress: address(rewarder), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saRewarderSelectors + }) + ); + + diamondCut[4] = ( + IDiamond.FacetCut({ + facetAddress: address(checkpointer), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saCheckpointerSelectors + }) + ); + + diamondCut[5] = ( + IDiamond.FacetCut({ + facetAddress: address(cutter), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saCutterSelectors + }) + ); + + diamondCut[6] = ( IDiamond.FacetCut({ facetAddress: address(louper), action: IDiamond.FacetCutAction.Add, - functionSelectors: gwLoupeSelectors + functionSelectors: saLouperSelectors }) ); @@ -419,10 +492,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, saManager = new SubnetActorManagerFacet(); saGetter = new SubnetActorGetterFacet(); + saPauser = new SubnetActorPauseFacet(); + saCheckpointer = new SubnetActorCheckpointingFacet(); + saRewarder = new SubnetActorRewardFacet(); saCutter = new DiamondCutFacet(); saLouper = new DiamondLoupeFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](4); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](7); diamondCut[0] = ( IDiamond.FacetCut({ @@ -441,6 +517,30 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, ); diamondCut[2] = ( + IDiamond.FacetCut({ + facetAddress: address(saPauser), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saPauserSelectors + }) + ); + + diamondCut[3] = ( + IDiamond.FacetCut({ + facetAddress: address(saRewarder), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saRewarderSelectors + }) + ); + + diamondCut[4] = ( + IDiamond.FacetCut({ + facetAddress: address(saCheckpointer), + action: IDiamond.FacetCutAction.Add, + functionSelectors: saCheckpointerSelectors + }) + ); + + diamondCut[5] = ( IDiamond.FacetCut({ facetAddress: address(saCutter), action: IDiamond.FacetCutAction.Add, @@ -448,7 +548,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); - diamondCut[3] = ( + diamondCut[6] = ( IDiamond.FacetCut({ facetAddress: address(saLouper), action: IDiamond.FacetCutAction.Add, @@ -475,13 +575,16 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, ); saManager = SubnetActorManagerFacet(address(saDiamond)); + saPauser = SubnetActorPauseFacet(address(saDiamond)); + saRewarder = SubnetActorRewardFacet(address(saDiamond)); + saCheckpointer = SubnetActorCheckpointingFacet(address(saDiamond)); saGetter = SubnetActorGetterFacet(address(saDiamond)); saCutter = DiamondCutFacet(address(saDiamond)); saLouper = DiamondLoupeFacet(address(saDiamond)); } function createMockedSubnetActorWithGateway(address gw) public returns (SubnetActorDiamond) { - SubnetActorManagerFacetMock mockedManager = new SubnetActorManagerFacetMock(); + SubnetActorMock mockedManager = new SubnetActorMock(); SubnetActorGetterFacet getter = new SubnetActorGetterFacet(); IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](2); @@ -749,7 +852,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, for (uint256 i = 0; i < n; i++) { vm.prank(validators[i]); - saManager.submitCheckpoint(checkpoint, validators, signatures); + saCheckpointer.submitCheckpoint(checkpoint, validators, signatures); } } diff --git a/test/helpers/SelectorLibrary.sol b/test/helpers/SelectorLibrary.sol index a17aa04f8..f98acb0aa 100644 --- a/test/helpers/SelectorLibrary.sol +++ b/test/helpers/SelectorLibrary.sol @@ -97,7 +97,28 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorManagerFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001210fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000ed7c4da1000000000000000000000000000000000000000000000000000000004c860af6000000000000000000000000000000000000000000000000000000006170b1620000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000008456cb59000000000000000000000000000000000000000000000000000000005c975abb000000000000000000000000000000000000000000000000000000000b7fbe600000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee000000000000000000000000000000000000000000000000000000003a4b66f1000000000000000000000000000000000000000000000000000000002681193600000000000000000000000000000000000000000000000000000000b9ee2bb9000000000000000000000000000000000000000000000000000000003f4ba83a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000910fd4261000000000000000000000000000000000000000000000000000000006170b1620000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000000b7fbe600000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee000000000000000000000000000000000000000000000000000000003a4b66f1000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorPauseFacet"))) { + return + abi.decode( + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000038456cb59000000000000000000000000000000000000000000000000000000005c975abb000000000000000000000000000000000000000000000000000000003f4ba83a00000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorRewardFacet"))) { + return + abi.decode( + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000034e71d92d00000000000000000000000000000000000000000000000000000000ed7c4da1000000000000000000000000000000000000000000000000000000004c860af600000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { + return + abi.decode( + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000032681193600000000000000000000000000000000000000000000000000000000b9ee2bb900000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -150,7 +171,7 @@ library SelectorLibrary { (bytes4[]) ); } - if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorManagerFacetMock"))) { + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorMock"))) { return abi.decode( hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001410fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000ed7c4da100000000000000000000000000000000000000000000000000000000350a14bf00000000000000000000000000000000000000000000000000000000c7ebdaef000000000000000000000000000000000000000000000000000000004c860af6000000000000000000000000000000000000000000000000000000006170b1620000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000008456cb59000000000000000000000000000000000000000000000000000000005c975abb000000000000000000000000000000000000000000000000000000000b7fbe600000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee000000000000000000000000000000000000000000000000000000003a4b66f1000000000000000000000000000000000000000000000000000000002681193600000000000000000000000000000000000000000000000000000000b9ee2bb9000000000000000000000000000000000000000000000000000000003f4ba83a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", diff --git a/test/integration/SubnetActorDiamond.t.sol b/test/integration/SubnetActorDiamond.t.sol index 9bcca7cef..0e1acb37f 100644 --- a/test/integration/SubnetActorDiamond.t.sol +++ b/test/integration/SubnetActorDiamond.t.sol @@ -26,6 +26,9 @@ import {SubnetActorDiamond, FunctionNotFound} from "../../src/SubnetActorDiamond import {FEATURE_CHECKPOINT_RELAYER_REWARDS} from "../../src/GatewayDiamond.sol"; import {SubnetActorManagerFacet} from "../../src/subnet/SubnetActorManagerFacet.sol"; import {SubnetActorGetterFacet} from "../../src/subnet/SubnetActorGetterFacet.sol"; +import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; +import {SubnetActorRewardFacet} from "../../src/subnet/SubnetActorRewardFacet.sol"; import {DiamondCutFacet} from "../../src/diamond/DiamondCutFacet.sol"; import {FilAddress} from "fevmate/utils/FilAddress.sol"; import {LibStaking} from "../../src/lib/LibStaking.sol"; @@ -68,7 +71,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } function testSubnetActorDiamondReal_LoupeFunction() public view { - require(saLouper.facets().length == 4, "unexpected length"); + require(saLouper.facets().length == 7, "unexpected length"); require(saLouper.supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported"); require(saLouper.supportsInterface(type(IDiamondCut).interfaceId) == true, "IDiamondCut not supported"); require(saLouper.supportsInterface(type(IDiamondLoupe).interfaceId) == true, "IDiamondLoupe not supported"); @@ -263,7 +266,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // ======== Step. Claim collateral ====== uint256 b1 = validator1.balance; vm.prank(validator1); - saManager.claim(); + saRewarder.claim(); uint256 b2 = validator1.balance; require(b2 - b1 == validator1Stake + stake, "collateral not received"); } @@ -298,8 +301,11 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { function testSubnetActorDiamond_Deployments_Fail_GatewayCannotBeZero() public { SubnetActorManagerFacet saDupMangerFaucet = new SubnetActorManagerFacet(); - SubnetActorGetterFacet saDupGetterFaucet = new SubnetActorGetterFacet(); + SubnetActorPauseFacet saDupPauserFaucet = new SubnetActorPauseFacet(); + SubnetActorRewardFacet saDupRewardFaucet = new SubnetActorRewardFacet(); + SubnetActorCheckpointingFacet saDupCheckpointerFaucet = new SubnetActorCheckpointingFacet(); + SupplySource memory native = SupplySourceHelper.native(); vm.expectRevert(GatewayCannotBeZero.selector); @@ -319,7 +325,10 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { supplySource: native }), address(saDupGetterFaucet), - address(saDupMangerFaucet) + address(saDupMangerFaucet), + address(saDupPauserFaucet), + address(saDupRewardFaucet), + address(saDupCheckpointerFaucet) ); } @@ -492,7 +501,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { saManager.join{value: 10}(pubKeys[i]); } - saManager.validateActiveQuorumSignatures(validators, hash, signatures); + saCheckpointer.validateActiveQuorumSignatures(validators, hash, signatures); } function testSubnetActorDiamond_validateActiveQuorumSignatures_InvalidWeightSum() public { @@ -522,7 +531,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { MultisignatureChecker.Error.WeightsSumLessThanThreshold ) ); - saManager.validateActiveQuorumSignatures(subValidators, hash, signatures); + saCheckpointer.validateActiveQuorumSignatures(subValidators, hash, signatures); } function testSubnetActorDiamond_validateActiveQuorumSignatures_InvalidSignature() public { @@ -549,7 +558,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert( abi.encodeWithSelector(InvalidSignatureErr.selector, MultisignatureChecker.Error.InvalidSignature) ); - saManager.validateActiveQuorumSignatures(validators, hash, signatures); + saCheckpointer.validateActiveQuorumSignatures(validators, hash, signatures); } function testSubnetActorDiamond_validateActiveQuorumSignatures_EmptySignatures() public { @@ -570,7 +579,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert( abi.encodeWithSelector(InvalidSignatureErr.selector, MultisignatureChecker.Error.EmptySignatures) ); - saManager.validateActiveQuorumSignatures(validators, hash, signatures); + saCheckpointer.validateActiveQuorumSignatures(validators, hash, signatures); } function testSubnetActorDiamond_validateActiveQuorumSignatures_InvalidArrayLength() public { @@ -591,7 +600,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert( abi.encodeWithSelector(InvalidSignatureErr.selector, MultisignatureChecker.Error.InvalidArrayLength) ); - saManager.validateActiveQuorumSignatures(validators, hash, signatures); + saCheckpointer.validateActiveQuorumSignatures(validators, hash, signatures); } function testSubnetActorDiamond_validateActiveQuorumSignatures_InvalidSignatory() public { @@ -623,7 +632,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert( abi.encodeWithSelector(InvalidSignatureErr.selector, MultisignatureChecker.Error.InvalidSignatory) ); - saManager.validateActiveQuorumSignatures(validators, hash0, signatures); + saCheckpointer.validateActiveQuorumSignatures(validators, hash0, signatures); } function testSubnetActorDiamond_submitCheckpoint_basic() public { @@ -683,11 +692,11 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert(InvalidCheckpointEpoch.selector); vm.prank(validators[0]); - saManager.submitCheckpoint(checkpointWithIncorrectHeight, validators, signatures); + saCheckpointer.submitCheckpoint(checkpointWithIncorrectHeight, validators, signatures); vm.expectCall(gatewayAddress, abi.encodeCall(IGateway.commitCheckpoint, (checkpoint)), 1); vm.prank(validators[0]); - saManager.submitCheckpoint(checkpoint, validators, signatures); + saCheckpointer.submitCheckpoint(checkpoint, validators, signatures); require(saGetter.hasSubmittedInLastBottomUpCheckpointHeight(validators[0]), "validator rewarded"); require( @@ -696,7 +705,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { ); vm.prank(validators[0]); - saManager.submitCheckpoint(checkpoint, validators, signatures); + saCheckpointer.submitCheckpoint(checkpoint, validators, signatures); require(saGetter.hasSubmittedInLastBottomUpCheckpointHeight(validators[0]), "validator rewarded"); require( saGetter.lastBottomUpCheckpointHeight() == saGetter.bottomUpCheckPeriod(), @@ -766,7 +775,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectCall(gatewayAddress, abi.encodeCall(IGateway.commitCheckpoint, (checkpoint)), 1); vm.prank(validators[0]); - saManager.submitCheckpoint(checkpoint, validators, signatures); + saCheckpointer.submitCheckpoint(checkpoint, validators, signatures); require(saGetter.hasSubmittedInLastBottomUpCheckpointHeight(validators[0]), "validator rewarded"); require( @@ -809,7 +818,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } vm.prank(validators[0]); - saManager.submitCheckpoint(checkpoint, validators, signatures); + saCheckpointer.submitCheckpoint(checkpoint, validators, signatures); require(saGetter.getRelayerReward(validators[1]) == 0, "unexpected reward"); require(saGetter.getRelayerReward(validators[2]) == 0, "unexpected reward"); @@ -821,7 +830,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // disable the claim of rewards if the fee is zero if (DEFAULT_CROSS_MSG_FEE != 0) { vm.startPrank(validators[0]); - saManager.claimRewardForRelayer(); + saRewarder.claimRewardForRelayer(); uint256 b2 = validators[0].balance; require(b2 - b1 == validator0Reward, "reward received"); } @@ -885,23 +894,23 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { vm.expectRevert(InvalidBatchEpoch.selector); vm.prank(validators[0]); - saManager.submitBottomUpMsgBatch(batchIncorrectHeight, validators, signatures); + saCheckpointer.submitBottomUpMsgBatch(batchIncorrectHeight, validators, signatures); vm.prank(validators[0]); batchIncorrectHeight.msgs = new CrossMsg[](0); batchIncorrectHeight.blockHeight = saGetter.bottomUpMsgBatchPeriod(); vm.expectRevert(BatchWithNoMessages.selector); - saManager.submitBottomUpMsgBatch(batchIncorrectHeight, validators, signatures); + saCheckpointer.submitBottomUpMsgBatch(batchIncorrectHeight, validators, signatures); vm.expectCall(gatewayAddress, abi.encodeCall(IGateway.execBottomUpMsgBatch, (batch)), 1); vm.prank(validators[0]); - saManager.submitBottomUpMsgBatch(batch, validators, signatures); + saCheckpointer.submitBottomUpMsgBatch(batch, validators, signatures); require(saGetter.hasSubmittedInLastBottomUpMsgBatchHeight(validators[0]), "validator rewarded"); require(saGetter.lastBottomUpMsgBatchHeight() == saGetter.bottomUpMsgBatchPeriod(), " batch height correct"); vm.prank(validators[1]); - saManager.submitBottomUpMsgBatch(batch, validators, signatures); + saCheckpointer.submitBottomUpMsgBatch(batch, validators, signatures); require(saGetter.hasSubmittedInLastBottomUpMsgBatchHeight(validators[0]), "validator rewarded"); require(saGetter.lastBottomUpMsgBatchHeight() == saGetter.bottomUpMsgBatchPeriod(), " batch height correct"); } @@ -1531,15 +1540,15 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } function testSubnetActorDiamond_Pausable_SetPaused() public { - saManager.pause(); - require(saManager.paused()); + saPauser.pause(); + require(saPauser.paused()); - saManager.unpause(); - require(!saManager.paused()); + saPauser.unpause(); + require(!saPauser.paused()); } function testSubnetActorDiamond_Pausable_EnforcedPause() public { - saManager.pause(); + saPauser.pause(); uint256 n = 1; (address[] memory validators, , bytes[] memory publicKeys) = TestUtils.newValidators(n); @@ -1552,18 +1561,18 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { function testSubnetActorDiamond_Pausable_NotOwner() public { vm.startPrank(address(1)); vm.expectRevert(LibDiamond.NotOwner.selector); - saManager.pause(); + saPauser.pause(); } function testSubnetActorDiamond_Pausable_CannotPauseAgain() public { - saManager.pause(); + saPauser.pause(); vm.expectRevert(Pausable.EnforcedPause.selector); - saManager.pause(); + saPauser.pause(); } function testSubnetActorDiamond_Pausable_CannotUnpauseAgain() public { vm.expectRevert(Pausable.ExpectedPause.selector); - saManager.unpause(); + saPauser.unpause(); } } diff --git a/test/integration/SubnetRegistry.t.sol b/test/integration/SubnetRegistry.t.sol index 570aedecb..3a0c56674 100644 --- a/test/integration/SubnetRegistry.t.sol +++ b/test/integration/SubnetRegistry.t.sol @@ -14,6 +14,9 @@ import {LibDiamond} from "../../src/lib/LibDiamond.sol"; import {SubnetActorGetterFacet} from "../../src/subnet/SubnetActorGetterFacet.sol"; import {SubnetActorManagerFacet} from "../../src/subnet/SubnetActorManagerFacet.sol"; +import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; +import {SubnetActorRewardFacet} from "../../src/subnet/SubnetActorRewardFacet.sol"; import {SubnetActorDiamond} from "../../src/SubnetActorDiamond.sol"; import {SubnetID, PermissionMode} from "../../src/structs/Subnet.sol"; import {SubnetRegistryDiamond} from "../../src/SubnetRegistryDiamond.sol"; diff --git a/test/invariants/SubnetActorInvariants.t.sol b/test/invariants/SubnetActorInvariants.t.sol index 6f3fab732..837ee44ac 100644 --- a/test/invariants/SubnetActorInvariants.t.sol +++ b/test/invariants/SubnetActorInvariants.t.sol @@ -10,7 +10,7 @@ import {GatewayGetterFacet} from "../../src/gateway/GatewayGetterFacet.sol"; import {GatewayMessengerFacet} from "../../src/gateway/GatewayMessengerFacet.sol"; import {GatewayManagerFacet} from "../../src/gateway/GatewayManagerFacet.sol"; import {SubnetActorHandler, ETH_SUPPLY} from "./handlers/SubnetActorHandler.sol"; -import {SubnetActorManagerFacetMock} from "../mocks/SubnetActorManagerFacetMock.sol"; +import {SubnetActorMock} from "../mocks/SubnetActorMock.sol"; import {SubnetActorGetterFacet} from "../../src/subnet/SubnetActorGetterFacet.sol"; import {IntegrationTestBase} from "../IntegrationTestBase.sol"; import {SupplySourceHelper} from "../../src/lib/SupplySourceHelper.sol"; @@ -34,7 +34,7 @@ contract SubnetActorInvariants is StdInvariant, IntegrationTestBase { saDiamond = createMockedSubnetActorWithGateway(gatewayAddress); - saMockedManager = SubnetActorManagerFacetMock(address(saDiamond)); + saMock = SubnetActorMock(address(saDiamond)); saGetter = SubnetActorGetterFacet(address(saDiamond)); subnetActorHandler = new SubnetActorHandler(saDiamond); @@ -120,8 +120,8 @@ contract SubnetActorInvariants is StdInvariant, IntegrationTestBase { uint256 balanceBefore = validator.balance; vm.prank(validator); - saMockedManager.claim(); - saMockedManager.confirmNextChange(); + saMock.claim(); + saMock.confirmNextChange(); uint256 balanceAfter = validator.balance; uint256 subnetBalanceAfter = address(saDiamond).balance; diff --git a/test/invariants/SubnetRegistryInvariants.t.sol b/test/invariants/SubnetRegistryInvariants.t.sol index 945121606..5d7eba2ca 100644 --- a/test/invariants/SubnetRegistryInvariants.t.sol +++ b/test/invariants/SubnetRegistryInvariants.t.sol @@ -9,6 +9,9 @@ import {SubnetRegistryDiamond} from "../../src/SubnetRegistryDiamond.sol"; import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol"; import {SubnetActorGetterFacet} from "../../src/subnet/SubnetActorGetterFacet.sol"; import {SubnetActorManagerFacet} from "../../src/subnet/SubnetActorManagerFacet.sol"; +import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; +import {SubnetActorRewardFacet} from "../../src/subnet/SubnetActorRewardFacet.sol"; import {SubnetID} from "../../src/structs/Subnet.sol"; import {RegisterSubnetFacet} from "../../src/subnetregistry/RegisterSubnetFacet.sol"; import {SubnetGetterFacet} from "../../src/subnetregistry/SubnetGetterFacet.sol"; diff --git a/test/invariants/handlers/SubnetActorHandler.sol b/test/invariants/handlers/SubnetActorHandler.sol index a860b8beb..54804f763 100644 --- a/test/invariants/handlers/SubnetActorHandler.sol +++ b/test/invariants/handlers/SubnetActorHandler.sol @@ -6,7 +6,7 @@ import "forge-std/StdCheats.sol"; import {CommonBase} from "forge-std/Base.sol"; import {SubnetActorDiamond} from "../../../src/SubnetActorDiamond.sol"; import {SubnetActorGetterFacet} from "../../../src/subnet/SubnetActorGetterFacet.sol"; -import {SubnetActorManagerFacetMock} from "../../mocks/SubnetActorManagerFacetMock.sol"; +import {SubnetActorMock} from "../../mocks/SubnetActorMock.sol"; import {TestUtils} from "../../helpers/TestUtils.sol"; import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; @@ -15,7 +15,7 @@ uint256 constant ETH_SUPPLY = 129_590_000 ether; contract SubnetActorHandler is CommonBase, StdCheats, StdUtils { using EnumerableSet for EnumerableSet.AddressSet; - SubnetActorManagerFacetMock private managerFacet; + SubnetActorMock private managerFacet; SubnetActorGetterFacet private getterFacet; uint256 private constant DEFAULT_MIN_VALIDATOR_STAKE = 10 ether; @@ -32,7 +32,7 @@ contract SubnetActorHandler is CommonBase, StdCheats, StdUtils { uint256 public ghost_unstakedSum; constructor(SubnetActorDiamond _subnetActor) { - managerFacet = SubnetActorManagerFacetMock(address(_subnetActor)); + managerFacet = SubnetActorMock(address(_subnetActor)); getterFacet = SubnetActorGetterFacet(address(_subnetActor)); deal(address(this), ETH_SUPPLY); diff --git a/test/mocks/SubnetActorManagerFacetMock.sol b/test/mocks/SubnetActorMock.sol similarity index 56% rename from test/mocks/SubnetActorManagerFacetMock.sol rename to test/mocks/SubnetActorMock.sol index 70685b49b..8950dedda 100644 --- a/test/mocks/SubnetActorManagerFacetMock.sol +++ b/test/mocks/SubnetActorMock.sol @@ -3,8 +3,16 @@ pragma solidity 0.8.19; import {SubnetActorManagerFacet} from "../../src/subnet/SubnetActorManagerFacet.sol"; import {LibStaking} from "../../src/lib/LibStaking.sol"; +import {SubnetActorPauseFacet} from "../../src/subnet/SubnetActorPauseFacet.sol"; +import {SubnetActorRewardFacet} from "../../src/subnet/SubnetActorRewardFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../../src/subnet/SubnetActorCheckpointingFacet.sol"; -contract SubnetActorManagerFacetMock is SubnetActorManagerFacet { +contract SubnetActorMock is + SubnetActorPauseFacet, + SubnetActorManagerFacet, + SubnetActorRewardFacet, + SubnetActorCheckpointingFacet +{ function confirmChange(uint64 _configurationNumber) external { LibStaking.confirmChange(_configurationNumber); }