Repository: darcius/rocketpool Branch: master Commit: fef41a4f7cf9 Files: 308 Total size: 2.0 MB Directory structure: gitextract_0qumgsk9/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── CI.yml ├── .gitignore ├── .mocharc.json ├── LICENSE ├── README.md ├── contracts/ │ ├── .gitattributes │ ├── contract/ │ │ ├── RocketBase.sol │ │ ├── RocketStorage.sol │ │ ├── RocketVault.sol │ │ ├── auction/ │ │ │ └── RocketAuctionManager.sol │ │ ├── casper/ │ │ │ └── compiled/ │ │ │ └── Deposit.abi │ │ ├── dao/ │ │ │ ├── RocketDAOProposal.sol │ │ │ ├── node/ │ │ │ │ ├── RocketDAONodeTrusted.sol │ │ │ │ ├── RocketDAONodeTrustedActions.sol │ │ │ │ ├── RocketDAONodeTrustedProposals.sol │ │ │ │ ├── RocketDAONodeTrustedUpgrade.sol │ │ │ │ └── settings/ │ │ │ │ ├── RocketDAONodeTrustedSettings.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsMembers.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsMinipool.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsProposals.sol │ │ │ │ └── RocketDAONodeTrustedSettingsRewards.sol │ │ │ ├── protocol/ │ │ │ │ ├── RocketDAOProtocol.sol │ │ │ │ ├── RocketDAOProtocolActions.sol │ │ │ │ ├── RocketDAOProtocolProposal.sol │ │ │ │ ├── RocketDAOProtocolProposals.sol │ │ │ │ ├── RocketDAOProtocolVerifier.sol │ │ │ │ └── settings/ │ │ │ │ ├── RocketDAOProtocolSettings.sol │ │ │ │ ├── RocketDAOProtocolSettingsAuction.sol │ │ │ │ ├── RocketDAOProtocolSettingsDeposit.sol │ │ │ │ ├── RocketDAOProtocolSettingsInflation.sol │ │ │ │ ├── RocketDAOProtocolSettingsMegapool.sol │ │ │ │ ├── RocketDAOProtocolSettingsMinipool.sol │ │ │ │ ├── RocketDAOProtocolSettingsNetwork.sol │ │ │ │ ├── RocketDAOProtocolSettingsNode.sol │ │ │ │ ├── RocketDAOProtocolSettingsProposals.sol │ │ │ │ ├── RocketDAOProtocolSettingsRewards.sol │ │ │ │ └── RocketDAOProtocolSettingsSecurity.sol │ │ │ └── security/ │ │ │ ├── RocketDAOSecurity.sol │ │ │ ├── RocketDAOSecurityActions.sol │ │ │ ├── RocketDAOSecurityProposals.sol │ │ │ └── RocketDAOSecurityUpgrade.sol │ │ ├── deposit/ │ │ │ └── RocketDepositPool.sol │ │ ├── helper/ │ │ │ ├── BeaconStateVerifierMock.sol │ │ │ ├── MegapoolUpgradeHelper.sol │ │ │ ├── PenaltyTest.sol │ │ │ ├── RevertOnTransfer.sol │ │ │ ├── SnapshotTest.sol │ │ │ ├── SnapshotTimeTest.sol │ │ │ ├── StakeHelper.sol │ │ │ └── StorageHelper.sol │ │ ├── megapool/ │ │ │ ├── RocketMegapoolDelegate.sol │ │ │ ├── RocketMegapoolDelegateBase.sol │ │ │ ├── RocketMegapoolFactory.sol │ │ │ ├── RocketMegapoolManager.sol │ │ │ ├── RocketMegapoolPenalties.sol │ │ │ ├── RocketMegapoolProxy.sol │ │ │ └── RocketMegapoolStorageLayout.sol │ │ ├── minipool/ │ │ │ ├── RocketMinipoolBase.sol │ │ │ ├── RocketMinipoolBondReducer.sol │ │ │ ├── RocketMinipoolDelegate.sol │ │ │ ├── RocketMinipoolFactory.sol │ │ │ ├── RocketMinipoolManager.sol │ │ │ ├── RocketMinipoolPenalty.sol │ │ │ ├── RocketMinipoolQueue.sol │ │ │ └── RocketMinipoolStorageLayout.sol │ │ ├── network/ │ │ │ ├── RocketNetworkBalances.sol │ │ │ ├── RocketNetworkFees.sol │ │ │ ├── RocketNetworkPenalties.sol │ │ │ ├── RocketNetworkPrices.sol │ │ │ ├── RocketNetworkRevenues.sol │ │ │ ├── RocketNetworkSnapshots.sol │ │ │ ├── RocketNetworkSnapshotsTime.sol │ │ │ └── RocketNetworkVoting.sol │ │ ├── node/ │ │ │ ├── RocketNodeDeposit.sol │ │ │ ├── RocketNodeDistributor.sol │ │ │ ├── RocketNodeDistributorDelegate.sol │ │ │ ├── RocketNodeDistributorFactory.sol │ │ │ ├── RocketNodeDistributorStorageLayout.sol │ │ │ ├── RocketNodeManager.sol │ │ │ └── RocketNodeStaking.sol │ │ ├── rewards/ │ │ │ ├── RocketClaimDAO.sol │ │ │ ├── RocketMerkleDistributorMainnet.sol │ │ │ ├── RocketRewardsPool.sol │ │ │ └── RocketSmoothingPool.sol │ │ ├── token/ │ │ │ ├── RocketTokenRETH.sol │ │ │ ├── RocketTokenRPL.sol │ │ │ └── temp/ │ │ │ └── RocketTokenDummyRPL.sol │ │ └── util/ │ │ ├── AddressQueueStorage.sol │ │ ├── AddressSetStorage.sol │ │ ├── BeaconStateVerifier.sol │ │ ├── Context.sol │ │ ├── ERC20.sol │ │ ├── ERC20Burnable.sol │ │ ├── LinkedListStorage.sol │ │ ├── LinkedListStorageHelper.sol │ │ ├── SSZ.sol │ │ ├── SafeERC20.sol │ │ └── SafeMath.sol │ ├── interface/ │ │ ├── RocketStorageInterface.sol │ │ ├── RocketVaultInterface.sol │ │ ├── RocketVaultWithdrawerInterface.sol │ │ ├── auction/ │ │ │ └── RocketAuctionManagerInterface.sol │ │ ├── casper/ │ │ │ └── DepositInterface.sol │ │ ├── dao/ │ │ │ ├── RocketDAOProposalInterface.sol │ │ │ ├── node/ │ │ │ │ ├── RocketDAONodeTrustedActionsInterface.sol │ │ │ │ ├── RocketDAONodeTrustedInterface.sol │ │ │ │ ├── RocketDAONodeTrustedProposalsInterface.sol │ │ │ │ ├── RocketDAONodeTrustedUpgradeInterface.sol │ │ │ │ └── settings/ │ │ │ │ ├── RocketDAONodeTrustedSettingsInterface.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsMembersInterface.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsMinipoolInterface.sol │ │ │ │ ├── RocketDAONodeTrustedSettingsProposalsInterface.sol │ │ │ │ └── RocketDAONodeTrustedSettingsRewardsInterface.sol │ │ │ ├── protocol/ │ │ │ │ ├── RocketDAOProtocolActionsInterface.sol │ │ │ │ ├── RocketDAOProtocolInterface.sol │ │ │ │ ├── RocketDAOProtocolProposalInterface.sol │ │ │ │ ├── RocketDAOProtocolProposalsInterface.sol │ │ │ │ ├── RocketDAOProtocolVerifierInterface.sol │ │ │ │ └── settings/ │ │ │ │ ├── RocketDAOProtocolSettingsAuctionInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsDepositInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsInflationInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsMegapoolInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsMinipoolInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsNetworkInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsNodeInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsProposalsInterface.sol │ │ │ │ ├── RocketDAOProtocolSettingsRewardsInterface.sol │ │ │ │ └── RocketDAOProtocolSettingsSecurityInterface.sol │ │ │ └── security/ │ │ │ ├── RocketDAOSecurityActionsInterface.sol │ │ │ ├── RocketDAOSecurityInterface.sol │ │ │ ├── RocketDAOSecurityProposalsInterface.sol │ │ │ └── RocketDAOSecurityUpgradeInterface.sol │ │ ├── deposit/ │ │ │ └── RocketDepositPoolInterface.sol │ │ ├── megapool/ │ │ │ ├── RocketMegapoolDelegateBaseInterface.sol │ │ │ ├── RocketMegapoolDelegateInterface.sol │ │ │ ├── RocketMegapoolFactoryInterface.sol │ │ │ ├── RocketMegapoolInterface.sol │ │ │ ├── RocketMegapoolManagerInterface.sol │ │ │ ├── RocketMegapoolPenaltiesInterface.sol │ │ │ └── RocketMegapoolProxyInterface.sol │ │ ├── minipool/ │ │ │ ├── RocketMinipoolBaseInterface.sol │ │ │ ├── RocketMinipoolBondReducerInterface.sol │ │ │ ├── RocketMinipoolFactoryInterface.sol │ │ │ ├── RocketMinipoolInterface.sol │ │ │ ├── RocketMinipoolManagerInterface.sol │ │ │ ├── RocketMinipoolPenaltyInterface.sol │ │ │ └── RocketMinipoolQueueInterface.sol │ │ ├── network/ │ │ │ ├── RocketNetworkBalancesInterface.sol │ │ │ ├── RocketNetworkFeesInterface.sol │ │ │ ├── RocketNetworkPenaltiesInterface.sol │ │ │ ├── RocketNetworkPricesInterface.sol │ │ │ ├── RocketNetworkRevenuesInterface.sol │ │ │ ├── RocketNetworkSnapshotsInterface.sol │ │ │ ├── RocketNetworkSnapshotsTimeInterface.sol │ │ │ └── RocketNetworkVotingInterface.sol │ │ ├── node/ │ │ │ ├── RocketNodeDepositInterface.sol │ │ │ ├── RocketNodeDistributorFactoryInterface.sol │ │ │ ├── RocketNodeDistributorInterface.sol │ │ │ ├── RocketNodeManagerInterface.sol │ │ │ └── RocketNodeStakingInterface.sol │ │ ├── rewards/ │ │ │ ├── RocketMerkleDistributorMainnetInterface.sol │ │ │ ├── RocketRewardsPoolInterface.sol │ │ │ ├── RocketRewardsRelayInterface.sol │ │ │ ├── RocketSmoothingPoolInterface.sol │ │ │ └── claims/ │ │ │ ├── RocketClaimDAOInterface.sol │ │ │ ├── RocketClaimNodeInterface.sol │ │ │ └── RocketClaimTrustedNodeInterface.sol │ │ ├── token/ │ │ │ ├── RocketTokenRETHInterface.sol │ │ │ └── RocketTokenRPLInterface.sol │ │ └── util/ │ │ ├── AddressQueueStorageInterface.sol │ │ ├── AddressSetStorageInterface.sol │ │ ├── BeaconStateVerifierInterface.sol │ │ ├── IERC20.sol │ │ ├── IERC20Burnable.sol │ │ └── LinkedListStorageInterface.sol │ ├── thirdparty/ │ │ ├── EthBalanceChecker/ │ │ │ └── EthBalanceChecker.sol │ │ ├── Multicall2/ │ │ │ └── Multicall2.sol │ │ ├── RocketSignerRegistry/ │ │ │ ├── RocketSignerRegistry.sol │ │ │ └── interface/ │ │ │ └── RocketSignerRegistryInterface.sol │ │ └── UniswapOracleMock/ │ │ └── UniswapOracleMock.sol │ └── types/ │ ├── MinipoolDeposit.sol │ ├── MinipoolDetails.sol │ ├── MinipoolStatus.sol │ ├── NodeDetails.sol │ ├── RewardSubmission.sol │ └── SettingType.sol ├── hardhat-common.config.js ├── hardhat-deploy.config.js ├── hardhat.config.js ├── package.json ├── remapping.json ├── scripts/ │ ├── console.js │ ├── deploy-upgrade.v1.4.js │ ├── deploy.js │ ├── etherscan-verify.js │ ├── preamble.sol │ └── upgrade-test.sh └── test/ ├── _helpers/ │ ├── auction.js │ ├── beaconchain.js │ ├── bigmath.js │ ├── bn.js │ ├── console.js │ ├── dao.js │ ├── defaults.js │ ├── deployer.js │ ├── deployment.js │ ├── deposit.js │ ├── invariants.js │ ├── megapool.js │ ├── minipool.js │ ├── network.js │ ├── node.js │ ├── settings.js │ ├── tokens.js │ └── verify.js ├── _utils/ │ ├── artifacts.js │ ├── beacon.js │ ├── contract.js │ ├── evm.js │ ├── formatting.js │ ├── merkle-tree.js │ ├── snapshotting.js │ ├── testing.js │ └── upgrade.js ├── auction/ │ ├── auction-tests.js │ ├── scenario-claim-bid.js │ ├── scenario-create-lot.js │ ├── scenario-place-bid.js │ └── scenario-recover-rpl.js ├── dao/ │ ├── dao-node-trusted-tests.js │ ├── dao-protocol-tests.js │ ├── dao-protocol-treasury-tests.js │ ├── dao-security-tests.js │ ├── scenario-dao-node-trusted-bootstrap.js │ ├── scenario-dao-node-trusted.js │ ├── scenario-dao-proposal.js │ ├── scenario-dao-protocol-bootstrap.js │ ├── scenario-dao-protocol-treasury.js │ ├── scenario-dao-protocol.js │ ├── scenario-dao-security-upgrade.js │ └── scenario-dao-security.js ├── deposit/ │ ├── deposit-pool-tests.js │ ├── scenario-assign-deposits.js │ └── scenario-deposit.js ├── megapool/ │ ├── megapool-tests.js │ ├── scenario-apply-penalty.js │ ├── scenario-challenge.js │ ├── scenario-dissolve.js │ ├── scenario-distribute.js │ ├── scenario-exit-queue.js │ ├── scenario-exit.js │ ├── scenario-reduce-bond.js │ ├── scenario-repay-debt.js │ ├── scenario-stake.js │ └── scenario-withdraw-credit.js ├── minipool/ │ ├── minipool-scrub-tests.js │ ├── minipool-status-tests.js │ ├── minipool-tests.js │ ├── minipool-vacant-tests.js │ ├── minipool-withdrawal-tests.js │ ├── scenario-close.js │ ├── scenario-dissolve.js │ ├── scenario-reduce-bond.js │ ├── scenario-refund.js │ ├── scenario-scrub.js │ ├── scenario-skim-rewards.js │ ├── scenario-stake.js │ └── scenario-withdraw-validator-balance.js ├── network/ │ ├── network-balances-tests.js │ ├── network-fees-tests.js │ ├── network-prices-tests.js │ ├── network-revenues-tests.js │ ├── network-snapshots-tests.js │ ├── network-voting-tests.js │ ├── scenario-submit-balances.js │ ├── scenario-submit-penalties.js │ └── scenario-submit-prices.js ├── node/ │ ├── node-distributor-tests.js │ ├── node-manager-tests.js │ ├── node-staking-tests.js │ ├── scenario-deposit-v2.js │ ├── scenario-distribute-rewards.js │ ├── scenario-register-smoothing-pool.js │ ├── scenario-register.js │ ├── scenario-set-timezone.js │ ├── scenario-set-withdrawal-address.js │ ├── scenario-stake-rpl.js │ ├── scenario-unstake-legacy-rpl.js │ ├── scenario-unstake-rpl.js │ └── scenario-withdraw-rpl.js ├── rewards/ │ ├── rewards-tests.js │ ├── scenario-claim-and-stake-rewards.js │ ├── scenario-claim-rewards.js │ ├── scenario-rewards-claim.js │ └── scenario-submit-rewards.js ├── rocket-pool-tests.js ├── token/ │ ├── reth-tests.js │ ├── rpl-tests.js │ ├── scenario-reth-burn.js │ ├── scenario-reth-transfer.js │ ├── scenario-rpl-allow-fixed.js │ ├── scenario-rpl-burn-fixed.js │ ├── scenario-rpl-inflation.js │ └── scenario-rpl-mint-fixed.js └── util/ ├── util-tests.js └── verifier-tests.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.sol linguist-language=Solidity ================================================ FILE: .github/workflows/CI.yml ================================================ name: CI on: [push] jobs: run-ci: runs-on: ubuntu-latest steps: - uses: actions/setup-node@v3 with: node-version: 18 - uses: actions/checkout@v4 with: submodules: recursive - name: Install Components run: npm install - name: Run Rocket Pool tests run: npm test ================================================ FILE: .gitignore ================================================ node_modules/ .vscode .idea/ cache/ artifacts/ .env coverage/ coverage.json deployments/ ================================================ FILE: .mocharc.json ================================================ { "require": "hardhat/register", "timeout": 0 } ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ Rocket Pool - Decentralised Ethereum Liquid Staking Protocol # Rocket Pool - Decentralised Ethereum Liquid Staking Protocol Rocket Pool is a decentralized Ethereum liquid staking protocol. It lets people participate in Ethereum staking without needing to run a full validator with 32 ETH, and it also lowers the technical barrier for running nodes. Here’s a breakdown: ### For Regular Stakers - Users can stake as little as 0.01 ETH by depositing into Rocket Pool’s smart contracts - In return, they receive rETH (Rocket Pool ETH), a liquid staking token that automatically accrues staking rewards over time - rETH can be traded, used in DeFi, or redeemed for ETH + rewards ### For Node Operators - People who want to run validator nodes can join Rocket Pool by staking 8 ETH (instead of the full 32 ETH) - Rocket Pool pairs that 8 ETH with the ETH deposited by rETH users to make a full validator - They earn consensus rewards (ETH) plus commission from the rETH users for running the node ### Why It’s Different - Lower capital requirement, 8 ETH instead of 32 ETH - Better yield than solo staking for node operators - Decentralized alternative to centralized exchanges’ staking services - Permissionless: anyone can run a node, no approval required - rETH token means stakers don’t have their ETH locked; they can use rETH across DeFi Learn more at [https://rocketpool.net](https://rocketpool.net). # Test Rocket Pool Rocket Pool - Testing Ethereum Proof-of-Stake (PoS) Infrastructure Service and Pool for Ethereum 2.0 Beacon Chain To see Rocket Pool in action, clone the repo and run the test suite with the following commands: ```bash $ npm install $ npm test ``` Having issues? Have an idea? Interested in research? Our friendly community are available to help via our discord. ================================================ FILE: contracts/.gitattributes ================================================ *.sol linguist-language=Solidity ================================================ FILE: contracts/contract/RocketBase.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../interface/RocketStorageInterface.sol"; /// @title Base settings / modifiers for each contract in Rocket Pool /// @author David Rugendyke abstract contract RocketBase { // Calculate using this as the base uint256 constant calcBase = 1 ether; // Version of the contract uint8 public version; // The main storage contract where primary persistant storage is maintained RocketStorageInterface rocketStorage = RocketStorageInterface(address(0)); /*** Modifiers **********************************************************/ /** * @dev Throws if called by any sender that doesn't match a Rocket Pool network contract */ modifier onlyLatestNetworkContract() { require(getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))), "Invalid or outdated network contract"); _; } /** * @dev Throws if called by any sender that doesn't match one of the supplied contract or is the latest version of that contract */ modifier onlyLatestContract(string memory _contractName, address _contractAddress) { require(_contractAddress == getAddress(keccak256(abi.encodePacked("contract.address", _contractName))), "Invalid or outdated contract"); _; } /** * @dev Throws if called by any sender that isn't a registered node */ modifier onlyRegisteredNode(address _nodeAddress) { require(getBool(keccak256(abi.encodePacked("node.exists", _nodeAddress))), "Invalid node"); _; } /** * @dev Throws if called by any sender that isn't a trusted node DAO member */ modifier onlyTrustedNode(address _nodeAddress) { require(getBool(keccak256(abi.encodePacked("dao.trustednodes.", "member", _nodeAddress))), "Invalid trusted node"); _; } /** * @dev Throws if called by any sender that isn't a registered minipool */ modifier onlyRegisteredMinipool(address _minipoolAddress) { require(getBool(keccak256(abi.encodePacked("minipool.exists", _minipoolAddress))), "Invalid minipool"); _; } /** * @dev Throws if called by any sender that isn't a registered megapool */ modifier onlyRegisteredMegapool(address _megapoolAddress) { require(getBool(keccak256(abi.encodePacked("megapool.exists", _megapoolAddress))), "Invalid megapool"); _; } /** * @dev Throws if called by any sender that isn't a registered minipool or megapool */ modifier onlyRegisteredMinipoolOrMegapool(address _caller) { require( getBool(keccak256(abi.encodePacked("megapool.exists", _caller))) || getBool(keccak256(abi.encodePacked("minipool.exists", _caller))) , "Invalid caller"); _; } /** * @dev Throws if called by any account other than a guardian account (temporary account allowed access to settings before DAO is fully enabled) */ modifier onlyGuardian() { require(msg.sender == rocketStorage.getGuardian(), "Account is not a temporary guardian"); _; } /*** Methods **********************************************************/ /// @dev Set the main Rocket Storage address constructor(RocketStorageInterface _rocketStorageAddress) { // Update the contract address rocketStorage = RocketStorageInterface(_rocketStorageAddress); } /// @dev Get the address of a network contract by name function getContractAddress(string memory _contractName) internal view returns (address) { // Get the current contract address address contractAddress = getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); // Check it require(contractAddress != address(0x0), "Contract not found"); // Return return contractAddress; } /// @dev Get the address of a network contract by name (returns address(0x0) instead of reverting if contract does not exist) function getContractAddressUnsafe(string memory _contractName) internal view returns (address) { // Get the current contract address address contractAddress = getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); // Return return contractAddress; } /// @dev Get the name of a network contract by address function getContractName(address _contractAddress) internal view returns (string memory) { // Get the contract name string memory contractName = getString(keccak256(abi.encodePacked("contract.name", _contractAddress))); // Check it require(bytes(contractName).length > 0, "Contract not found"); // Return return contractName; } /// @dev Get revert error message from a .call method function getRevertMsg(bytes memory _returnData) internal pure returns (string memory) { // If the _res length is less than 68, then the transaction failed silently (without a revert message) if (_returnData.length < 68) return "Transaction reverted silently"; assembly { // Slice the sighash. _returnData := add(_returnData, 0x04) } return abi.decode(_returnData, (string)); // All that remains is the revert string } /*** Rocket Storage Methods ****************************************/ // Note: Unused helpers have been removed to keep contract sizes down /// @dev Storage get methods function getAddress(bytes32 _key) internal view returns (address) { return rocketStorage.getAddress(_key); } function getUint(bytes32 _key) internal view returns (uint) { return rocketStorage.getUint(_key); } function getString(bytes32 _key) internal view returns (string memory) { return rocketStorage.getString(_key); } function getBytes(bytes32 _key) internal view returns (bytes memory) { return rocketStorage.getBytes(_key); } function getBool(bytes32 _key) internal view returns (bool) { return rocketStorage.getBool(_key); } function getInt(bytes32 _key) internal view returns (int) { return rocketStorage.getInt(_key); } function getBytes32(bytes32 _key) internal view returns (bytes32) { return rocketStorage.getBytes32(_key); } /// @dev Storage set methods function setAddress(bytes32 _key, address _value) internal { rocketStorage.setAddress(_key, _value); } function setUint(bytes32 _key, uint _value) internal { rocketStorage.setUint(_key, _value); } function setString(bytes32 _key, string memory _value) internal { rocketStorage.setString(_key, _value); } function setBytes(bytes32 _key, bytes memory _value) internal { rocketStorage.setBytes(_key, _value); } function setBool(bytes32 _key, bool _value) internal { rocketStorage.setBool(_key, _value); } function setInt(bytes32 _key, int _value) internal { rocketStorage.setInt(_key, _value); } function setBytes32(bytes32 _key, bytes32 _value) internal { rocketStorage.setBytes32(_key, _value); } /// @dev Storage delete methods function deleteAddress(bytes32 _key) internal { rocketStorage.deleteAddress(_key); } function deleteUint(bytes32 _key) internal { rocketStorage.deleteUint(_key); } function deleteString(bytes32 _key) internal { rocketStorage.deleteString(_key); } function deleteBytes(bytes32 _key) internal { rocketStorage.deleteBytes(_key); } function deleteBool(bytes32 _key) internal { rocketStorage.deleteBool(_key); } function deleteInt(bytes32 _key) internal { rocketStorage.deleteInt(_key); } function deleteBytes32(bytes32 _key) internal { rocketStorage.deleteBytes32(_key); } /// @dev Storage arithmetic methods function addUint(bytes32 _key, uint256 _amount) internal { rocketStorage.addUint(_key, _amount); } function subUint(bytes32 _key, uint256 _amount) internal { rocketStorage.subUint(_key, _amount); } } ================================================ FILE: contracts/contract/RocketStorage.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../interface/RocketStorageInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; /// @title The primary persistent storage for Rocket Pool /// @author David Rugendyke contract RocketStorage is RocketStorageInterface { // Events event NodeWithdrawalAddressSet(address indexed node, address indexed withdrawalAddress, uint256 time); event GuardianChanged(address oldGuardian, address newGuardian); // Libraries using SafeMath for uint256; // Storage maps mapping(bytes32 => string) private stringStorage; mapping(bytes32 => bytes) private bytesStorage; mapping(bytes32 => uint256) private uintStorage; mapping(bytes32 => int256) private intStorage; mapping(bytes32 => address) private addressStorage; mapping(bytes32 => bool) private booleanStorage; mapping(bytes32 => bytes32) private bytes32Storage; // Protected storage (not accessible by network contracts) mapping(address => address) private withdrawalAddresses; mapping(address => address) private pendingWithdrawalAddresses; // Guardian address address guardian; address newGuardian; // Flag storage has been initialised bool storageInit = false; /// @dev Only allow access from the latest version of a contract in the Rocket Pool network after deployment modifier onlyLatestRocketNetworkContract() { if (storageInit == true) { // Make sure the access is permitted to only contracts in our Dapp require(booleanStorage[keccak256(abi.encodePacked("contract.exists", msg.sender))], "Invalid or outdated network contract"); } else { // Only Dapp and the guardian account are allowed access during initialisation. // tx.origin is only safe to use in this case for deployment since no external contracts are interacted with require(( booleanStorage[keccak256(abi.encodePacked("contract.exists", msg.sender))] || tx.origin == guardian ), "Invalid or outdated network contract attempting access during deployment"); } _; } /// @dev Construct RocketStorage constructor() { // Set the guardian upon deployment guardian = msg.sender; } // Get guardian address function getGuardian() external override view returns (address) { return guardian; } // Transfers guardianship to a new address function setGuardian(address _newAddress) external override { // Check tx comes from current guardian require(msg.sender == guardian, "Is not guardian account"); // Store new address awaiting confirmation newGuardian = _newAddress; } // Confirms change of guardian function confirmGuardian() external override { // Check tx came from new guardian address require(msg.sender == newGuardian, "Confirmation must come from new guardian address"); // Store old guardian for event address oldGuardian = guardian; // Update guardian and clear storage guardian = newGuardian; delete newGuardian; // Emit event emit GuardianChanged(oldGuardian, guardian); } // Set this as being deployed now function getDeployedStatus() external override view returns (bool) { return storageInit; } // Set this as being deployed now function setDeployedStatus() external { // Only guardian can lock this down require(msg.sender == guardian, "Is not guardian account"); // Set it now storageInit = true; } // Protected storage // Get a node's withdrawal address function getNodeWithdrawalAddress(address _nodeAddress) public override view returns (address) { // If no withdrawal address has been set, return the nodes address address withdrawalAddress = withdrawalAddresses[_nodeAddress]; if (withdrawalAddress == address(0)) { return _nodeAddress; } return withdrawalAddress; } // Get a node's pending withdrawal address function getNodePendingWithdrawalAddress(address _nodeAddress) external override view returns (address) { return pendingWithdrawalAddresses[_nodeAddress]; } // Set a node's withdrawal address function setWithdrawalAddress(address _nodeAddress, address _newWithdrawalAddress, bool _confirm) external override { // Check new withdrawal address require(_newWithdrawalAddress != address(0x0), "Invalid withdrawal address"); // Confirm the transaction is from the node's current withdrawal address address withdrawalAddress = getNodeWithdrawalAddress(_nodeAddress); require(withdrawalAddress == msg.sender, "Only a tx from a node's withdrawal address can update it"); // Update immediately if confirmed if (_confirm) { updateWithdrawalAddress(_nodeAddress, _newWithdrawalAddress); } // Set pending withdrawal address if not confirmed else { pendingWithdrawalAddresses[_nodeAddress] = _newWithdrawalAddress; } } // Confirm a node's new withdrawal address function confirmWithdrawalAddress(address _nodeAddress) external override { // Get node by pending withdrawal address require(pendingWithdrawalAddresses[_nodeAddress] == msg.sender, "Confirmation must come from the pending withdrawal address"); delete pendingWithdrawalAddresses[_nodeAddress]; // Update withdrawal address updateWithdrawalAddress(_nodeAddress, msg.sender); } // Update a node's withdrawal address function updateWithdrawalAddress(address _nodeAddress, address _newWithdrawalAddress) private { // Set new withdrawal address withdrawalAddresses[_nodeAddress] = _newWithdrawalAddress; // Emit withdrawal address set event emit NodeWithdrawalAddressSet(_nodeAddress, _newWithdrawalAddress, block.timestamp); } /// @param _key The key for the record function getAddress(bytes32 _key) override external view returns (address r) { return addressStorage[_key]; } /// @param _key The key for the record function getUint(bytes32 _key) override external view returns (uint256 r) { return uintStorage[_key]; } /// @param _key The key for the record function getString(bytes32 _key) override external view returns (string memory) { return stringStorage[_key]; } /// @param _key The key for the record function getBytes(bytes32 _key) override external view returns (bytes memory) { return bytesStorage[_key]; } /// @param _key The key for the record function getBool(bytes32 _key) override external view returns (bool r) { return booleanStorage[_key]; } /// @param _key The key for the record function getInt(bytes32 _key) override external view returns (int r) { return intStorage[_key]; } /// @param _key The key for the record function getBytes32(bytes32 _key) override external view returns (bytes32 r) { return bytes32Storage[_key]; } /// @param _key The key for the record function setAddress(bytes32 _key, address _value) onlyLatestRocketNetworkContract override external { addressStorage[_key] = _value; } /// @param _key The key for the record function setUint(bytes32 _key, uint _value) onlyLatestRocketNetworkContract override external { uintStorage[_key] = _value; } /// @param _key The key for the record function setString(bytes32 _key, string calldata _value) onlyLatestRocketNetworkContract override external { stringStorage[_key] = _value; } /// @param _key The key for the record function setBytes(bytes32 _key, bytes calldata _value) onlyLatestRocketNetworkContract override external { bytesStorage[_key] = _value; } /// @param _key The key for the record function setBool(bytes32 _key, bool _value) onlyLatestRocketNetworkContract override external { booleanStorage[_key] = _value; } /// @param _key The key for the record function setInt(bytes32 _key, int _value) onlyLatestRocketNetworkContract override external { intStorage[_key] = _value; } /// @param _key The key for the record function setBytes32(bytes32 _key, bytes32 _value) onlyLatestRocketNetworkContract override external { bytes32Storage[_key] = _value; } /// @param _key The key for the record function deleteAddress(bytes32 _key) onlyLatestRocketNetworkContract override external { delete addressStorage[_key]; } /// @param _key The key for the record function deleteUint(bytes32 _key) onlyLatestRocketNetworkContract override external { delete uintStorage[_key]; } /// @param _key The key for the record function deleteString(bytes32 _key) onlyLatestRocketNetworkContract override external { delete stringStorage[_key]; } /// @param _key The key for the record function deleteBytes(bytes32 _key) onlyLatestRocketNetworkContract override external { delete bytesStorage[_key]; } /// @param _key The key for the record function deleteBool(bytes32 _key) onlyLatestRocketNetworkContract override external { delete booleanStorage[_key]; } /// @param _key The key for the record function deleteInt(bytes32 _key) onlyLatestRocketNetworkContract override external { delete intStorage[_key]; } /// @param _key The key for the record function deleteBytes32(bytes32 _key) onlyLatestRocketNetworkContract override external { delete bytes32Storage[_key]; } /// @param _key The key for the record /// @param _amount An amount to add to the record's value function addUint(bytes32 _key, uint256 _amount) onlyLatestRocketNetworkContract override external { uintStorage[_key] = uintStorage[_key].add(_amount); } /// @param _key The key for the record /// @param _amount An amount to subtract from the record's value function subUint(bytes32 _key, uint256 _amount) onlyLatestRocketNetworkContract override external { uintStorage[_key] = uintStorage[_key].sub(_amount); } } ================================================ FILE: contracts/contract/RocketVault.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "./RocketBase.sol"; import "./util/SafeERC20.sol"; import "../interface/RocketVaultInterface.sol"; import "../interface/RocketVaultWithdrawerInterface.sol"; import "../interface/util/IERC20Burnable.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // ETH and rETH are stored here to prevent contract upgrades from affecting balances // The RocketVault contract must not be upgraded contract RocketVault is RocketBase, RocketVaultInterface { // Libs using SafeMath for uint; using SafeERC20 for IERC20; // Network contract balances mapping(string => uint256) etherBalances; mapping(bytes32 => uint256) tokenBalances; // Events event EtherDeposited(string indexed by, uint256 amount, uint256 time); event EtherWithdrawn(string indexed by, uint256 amount, uint256 time); event TokenDeposited(bytes32 indexed by, address indexed tokenAddress, uint256 amount, uint256 time); event TokenWithdrawn(bytes32 indexed by, address indexed tokenAddress, uint256 amount, uint256 time); event TokenBurned(bytes32 indexed by, address indexed tokenAddress, uint256 amount, uint256 time); event TokenTransfer(bytes32 indexed by, bytes32 indexed to, address indexed tokenAddress, uint256 amount, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } // Get a contract's ETH balance by address function balanceOf(string memory _networkContractName) override external view returns (uint256) { // Return balance return etherBalances[_networkContractName]; } // Get the balance of a token held by a network contract function balanceOfToken(string memory _networkContractName, IERC20 _tokenAddress) override external view returns (uint256) { // Return balance return tokenBalances[keccak256(abi.encodePacked(_networkContractName, _tokenAddress))]; } // Accept an ETH deposit from a network contract // Only accepts calls from Rocket Pool network contracts function depositEther() override external payable onlyLatestNetworkContract { // Valid amount? require(msg.value > 0, "No valid amount of ETH given to deposit"); // Get contract key string memory contractName = getContractName(msg.sender); // Update contract balance etherBalances[contractName] = etherBalances[contractName].add(msg.value); // Emit ether deposited event emit EtherDeposited(contractName, msg.value, block.timestamp); } // Withdraw an amount of ETH to a network contract // Only accepts calls from Rocket Pool network contracts function withdrawEther(uint256 _amount) override external onlyLatestNetworkContract { // Valid amount? require(_amount > 0, "No valid amount of ETH given to withdraw"); // Get contract key string memory contractName = getContractName(msg.sender); // Check and update contract balance require(etherBalances[contractName] >= _amount, "Insufficient contract ETH balance"); etherBalances[contractName] = etherBalances[contractName].sub(_amount); // Withdraw RocketVaultWithdrawerInterface withdrawer = RocketVaultWithdrawerInterface(msg.sender); withdrawer.receiveVaultWithdrawalETH{value: _amount}(); // Emit ether withdrawn event emit EtherWithdrawn(contractName, _amount, block.timestamp); } // Accept an token deposit and assign its balance to a network contract (saves a large amount of gas this way through not needing a double token transfer via a network contract first) function depositToken(string memory _networkContractName, IERC20 _tokenContract, uint256 _amount) override external { // Valid amount? require(_amount > 0, "No valid amount of tokens given to deposit"); // Make sure the network contract is valid (will throw if not) require(getContractAddress(_networkContractName) != address(0x0), "Not a valid network contract"); // Get contract key bytes32 contractKey = keccak256(abi.encodePacked(_networkContractName, address(_tokenContract))); // Send the tokens to this contract now require(_tokenContract.transferFrom(msg.sender, address(this), _amount), "Token transfer was not successful"); // Update contract balance tokenBalances[contractKey] = tokenBalances[contractKey].add(_amount); // Emit token transfer emit TokenDeposited(contractKey, address(_tokenContract), _amount, block.timestamp); } // Withdraw an amount of a ERC20 token to an address // Only accepts calls from Rocket Pool network contracts function withdrawToken(address _withdrawalAddress, IERC20 _tokenAddress, uint256 _amount) override external onlyLatestNetworkContract { // Valid amount? require(_amount > 0, "No valid amount of tokens given to withdraw"); // Get contract key bytes32 contractKey = keccak256(abi.encodePacked(getContractName(msg.sender), _tokenAddress)); // Update balances tokenBalances[contractKey] = tokenBalances[contractKey].sub(_amount); // Get the token ERC20 instance IERC20 tokenContract = IERC20(_tokenAddress); // Withdraw to the desired address require(tokenContract.transfer(_withdrawalAddress, _amount), "Rocket Vault token withdrawal unsuccessful"); // Emit token withdrawn event emit TokenWithdrawn(contractKey, address(_tokenAddress), _amount, block.timestamp); } // Transfer token from one contract to another // Only accepts calls from Rocket Pool network contracts function transferToken(string memory _networkContractName, IERC20 _tokenAddress, uint256 _amount) override external onlyLatestNetworkContract { // Valid amount? require(_amount > 0, "No valid amount of tokens given to transfer"); // Make sure the network contract is valid (will throw if not) require(getContractAddress(_networkContractName) != address(0x0), "Not a valid network contract"); // Get contract keys bytes32 contractKeyFrom = keccak256(abi.encodePacked(getContractName(msg.sender), _tokenAddress)); bytes32 contractKeyTo = keccak256(abi.encodePacked(_networkContractName, _tokenAddress)); // Update balances tokenBalances[contractKeyFrom] = tokenBalances[contractKeyFrom].sub(_amount); tokenBalances[contractKeyTo] = tokenBalances[contractKeyTo].add(_amount); // Emit token withdrawn event emit TokenTransfer(contractKeyFrom, contractKeyTo, address(_tokenAddress), _amount, block.timestamp); } // Burns an amount of a token that implements a burn(uint256) method // Only accepts calls from Rocket Pool network contracts function burnToken(IERC20Burnable _tokenAddress, uint256 _amount) override external onlyLatestNetworkContract { // Get contract key bytes32 contractKey = keccak256(abi.encodePacked(getContractName(msg.sender), _tokenAddress)); // Update balances tokenBalances[contractKey] = tokenBalances[contractKey].sub(_amount); // Get the token ERC20 instance IERC20Burnable tokenContract = IERC20Burnable(_tokenAddress); // Burn the tokens tokenContract.burn(_amount); // Emit token burn event emit TokenBurned(contractKey, address(_tokenAddress), _amount, block.timestamp); } } ================================================ FILE: contracts/contract/auction/RocketAuctionManager.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "@openzeppelin/contracts/math/SafeMath.sol"; import "../RocketBase.sol"; import "../../interface/auction/RocketAuctionManagerInterface.sol"; import "../../interface/deposit/RocketDepositPoolInterface.sol"; import "../../interface/network/RocketNetworkPricesInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsAuctionInterface.sol"; import "../../interface/RocketVaultInterface.sol"; // Facilitates RPL liquidation auctions contract RocketAuctionManager is RocketBase, RocketAuctionManagerInterface { // Libs using SafeMath for uint; // Events event LotCreated(uint256 indexed lotIndex, address indexed by, uint256 rplAmount, uint256 time); event BidPlaced(uint256 indexed lotIndex, address indexed by, uint256 bidAmount, uint256 time); event BidClaimed(uint256 indexed lotIndex, address indexed by, uint256 bidAmount, uint256 rplAmount, uint256 time); event RPLRecovered(uint256 indexed lotIndex, address indexed by, uint256 rplAmount, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } // Get the total RPL balance of the contract function getTotalRPLBalance() override public view returns (uint256) { RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); return rocketVault.balanceOfToken("rocketAuctionManager", IERC20(getContractAddress("rocketTokenRPL"))); } // Get/set the allotted RPL balance of the contract function getAllottedRPLBalance() override public view returns (uint256) { return getUint(keccak256("auction.rpl.allotted")); } function increaseAllottedRPLBalance(uint256 _amount) private { addUint(keccak256(abi.encodePacked("auction.rpl.allotted")), _amount); } function decreaseAllottedRPLBalance(uint256 _amount) private { subUint(keccak256(abi.encodePacked("auction.rpl.allotted")), _amount); } // Get the remaining (unallotted) RPL balance of the contract function getRemainingRPLBalance() override public view returns (uint256) { return getTotalRPLBalance().sub(getAllottedRPLBalance()); } // Get/set the number of lots for auction function getLotCount() override public view returns (uint256) { return getUint(keccak256("auction.lots.count")); } function setLotCount(uint256 _amount) private { setUint(keccak256("auction.lots.count"), _amount); } // Get lot details function getLotExists(uint256 _index) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("auction.lot.exists", _index))); } function getLotStartBlock(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.block.start", _index))); } function getLotEndBlock(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.block.end", _index))); } function getLotStartPrice(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.price.start", _index))); } function getLotReservePrice(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.price.reserve", _index))); } function getLotTotalRPLAmount(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.rpl.total", _index))); } // Get/set the total ETH amount bid on a lot function getLotTotalBidAmount(uint256 _index) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.bid.total", _index))); } function increaseLotTotalBidAmount(uint256 _index, uint256 _amount) private { addUint(keccak256(abi.encodePacked("auction.lot.bid.total", _index)), _amount); } // Get/set the ETH amount bid on a lot by an address function getLotAddressBidAmount(uint256 _index, address _bidder) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("auction.lot.bid.address", _index, _bidder))); } function setLotAddressBidAmount(uint256 _index, address _bidder, uint256 _amount) private { setUint(keccak256(abi.encodePacked("auction.lot.bid.address", _index, _bidder)), _amount); } function increaseLotAddressBidAmount(uint256 _index, address _bidder, uint256 _amount) private { addUint(keccak256(abi.encodePacked("auction.lot.bid.address", _index, _bidder)), _amount); } // Get/set the lot's RPL recovered status function getLotRPLRecovered(uint256 _index) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("auction.lot.rpl.recovered", _index))); } function setLotRPLRecovered(uint256 _index, bool _recovered) private { setBool(keccak256(abi.encodePacked("auction.lot.rpl.recovered", _index)), _recovered); } // Get the RPL price for a lot at a block function getLotPriceAtBlock(uint256 _index, uint256 _block) override public view returns (uint256) { // Get lot parameters uint256 startBlock = getLotStartBlock(_index); uint256 endBlock = getLotEndBlock(_index); uint256 startPrice = getLotStartPrice(_index); uint256 endPrice = getLotReservePrice(_index); // Calculate & return if (_block <= startBlock) { return startPrice; } if (_block >= endBlock) { return endPrice; } uint256 tn = _block.sub(startBlock); uint256 td = endBlock.sub(startBlock); return startPrice.sub(startPrice.sub(endPrice).mul(tn).mul(tn).div(td).div(td)); } // Get the RPL price for a lot at the current block function getLotPriceAtCurrentBlock(uint256 _index) override public view returns (uint256) { return getLotPriceAtBlock(_index, block.number); } // Get the RPL price for a lot based on total ETH amount bid function getLotPriceByTotalBids(uint256 _index) override public view returns (uint256) { return calcBase.mul(getLotTotalBidAmount(_index)).div(getLotTotalRPLAmount(_index)); } // Get the current RPL price for a lot // Returns the clearing price if cleared, or the price at the current block otherwise function getLotCurrentPrice(uint256 _index) override public view returns (uint256) { uint256 blockPrice = getLotPriceAtCurrentBlock(_index); uint256 bidPrice = getLotPriceByTotalBids(_index); if (bidPrice > blockPrice) { return bidPrice; } else { return blockPrice; } } // Get the amount of claimed RPL in a lot function getLotClaimedRPLAmount(uint256 _index) override public view returns (uint256) { uint256 claimed = calcBase.mul(getLotTotalBidAmount(_index)).div(getLotCurrentPrice(_index)); uint256 total = getLotTotalRPLAmount(_index); // Due to integer arithmetic, the calculated claimed amount may be slightly greater than the total if (claimed > total) { return total; } return claimed; } // Get the amount of remaining RPL in a lot function getLotRemainingRPLAmount(uint256 _index) override public view returns (uint256) { return getLotTotalRPLAmount(_index).sub(getLotClaimedRPLAmount(_index)); } // Check whether a lot has cleared function getLotIsCleared(uint256 _index) override external view returns (bool) { if (block.number >= getLotEndBlock(_index)) { return true; } if (getLotPriceByTotalBids(_index) >= getLotPriceAtCurrentBlock(_index)) { return true; } return false; } // Create a new lot for auction function createLot() override external onlyLatestContract("rocketAuctionManager", address(this)) { // Load contracts RocketDAOProtocolSettingsAuctionInterface rocketAuctionSettings = RocketDAOProtocolSettingsAuctionInterface(getContractAddress("rocketDAOProtocolSettingsAuction")); RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices")); // Get remaining RPL balance & RPL price uint256 remainingRplBalance = getRemainingRPLBalance(); uint256 rplPrice = rocketNetworkPrices.getRPLPrice(); // Check lot can be created require(rocketAuctionSettings.getCreateLotEnabled(), "Creating lots is currently disabled"); require(remainingRplBalance >= calcBase.mul(rocketAuctionSettings.getLotMinimumEthValue()).div(rplPrice), "Insufficient RPL balance to create new lot"); // Calculate lot RPL amount uint256 lotRplAmount = remainingRplBalance; uint256 maximumLotRplAmount = calcBase.mul(rocketAuctionSettings.getLotMaximumEthValue()).div(rplPrice); if (lotRplAmount > maximumLotRplAmount) { lotRplAmount = maximumLotRplAmount; } // Create lot uint256 lotIndex = getLotCount(); setBool(keccak256(abi.encodePacked("auction.lot.exists", lotIndex)), true); setUint(keccak256(abi.encodePacked("auction.lot.block.start", lotIndex)), block.number); setUint(keccak256(abi.encodePacked("auction.lot.block.end", lotIndex)), block.number.add(rocketAuctionSettings.getLotDuration())); setUint(keccak256(abi.encodePacked("auction.lot.price.start", lotIndex)), rplPrice.mul(rocketAuctionSettings.getStartingPriceRatio()).div(calcBase)); setUint(keccak256(abi.encodePacked("auction.lot.price.reserve", lotIndex)), rplPrice.mul(rocketAuctionSettings.getReservePriceRatio()).div(calcBase)); setUint(keccak256(abi.encodePacked("auction.lot.rpl.total", lotIndex)), lotRplAmount); // Increment lot count & increase allotted RPL balance setLotCount(lotIndex.add(1)); increaseAllottedRPLBalance(lotRplAmount); // Emit lot created event emit LotCreated(lotIndex, msg.sender, lotRplAmount, block.timestamp); } // Bid on a lot function placeBid(uint256 _lotIndex) override external payable onlyLatestContract("rocketAuctionManager", address(this)) { // Load contracts RocketDAOProtocolSettingsAuctionInterface rocketAuctionSettings = RocketDAOProtocolSettingsAuctionInterface(getContractAddress("rocketDAOProtocolSettingsAuction")); RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); // Check bid amount require(msg.value > 0, "Invalid bid amount"); // Check lot exists require(getLotExists(_lotIndex), "Lot does not exist"); // Check lot can be bid on require(rocketAuctionSettings.getBidOnLotEnabled(), "Bidding on lots is currently disabled"); require(block.number < getLotEndBlock(_lotIndex), "Lot bidding period has concluded"); // Check lot has RPL remaining uint256 remainingRplAmount = getLotRemainingRPLAmount(_lotIndex); require(remainingRplAmount > 0, "Lot RPL allocation has been exhausted"); // Calculate the bid amount uint256 bidAmount = msg.value; uint256 maximumBidAmount = remainingRplAmount.mul(getLotPriceAtCurrentBlock(_lotIndex)).div(calcBase); if (bidAmount > maximumBidAmount) { bidAmount = maximumBidAmount; } // Increase lot bid amounts increaseLotTotalBidAmount(_lotIndex, bidAmount); increaseLotAddressBidAmount(_lotIndex, msg.sender, bidAmount); // Transfer bid amount to deposit pool rocketDepositPool.recycleLiquidatedStake{value: bidAmount}(); // Refund excess ETH to sender if (msg.value > bidAmount) { msg.sender.transfer(msg.value.sub(bidAmount)); } // Emit bid placed event emit BidPlaced(_lotIndex, msg.sender, bidAmount, block.timestamp); } // Claim RPL from a lot function claimBid(uint256 _lotIndex) override external onlyLatestContract("rocketAuctionManager", address(this)) { // Check lot exists require(getLotExists(_lotIndex), "Lot does not exist"); // Get lot price info uint256 blockPrice = getLotPriceAtCurrentBlock(_lotIndex); uint256 bidPrice = getLotPriceByTotalBids(_lotIndex); // Check lot can be claimed from require(block.number >= getLotEndBlock(_lotIndex) || bidPrice >= blockPrice, "Lot has not cleared yet"); // Get & check address bid amount uint256 bidAmount = getLotAddressBidAmount(_lotIndex, msg.sender); require(bidAmount > 0, "Address has no RPL to claim"); // Calculate current lot price uint256 currentPrice; if (bidPrice > blockPrice) { currentPrice = bidPrice; } else { currentPrice = blockPrice; } // Calculate RPL claim amount uint256 rplAmount = calcBase.mul(bidAmount).div(currentPrice); // Due to integer arithmetic, there may be a tiny bit less than calculated uint256 allottedAmount = getAllottedRPLBalance(); if (rplAmount > allottedAmount) { rplAmount = allottedAmount; } // Transfer RPL to bidder RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.withdrawToken(msg.sender, IERC20(getContractAddress("rocketTokenRPL")), rplAmount); // Decrease allotted RPL balance & update address bid amount decreaseAllottedRPLBalance(rplAmount); setLotAddressBidAmount(_lotIndex, msg.sender, 0); // Emit bid claimed event emit BidClaimed(_lotIndex, msg.sender, bidAmount, rplAmount, block.timestamp); } // Recover unclaimed RPL from a lot function recoverUnclaimedRPL(uint256 _lotIndex) override external onlyLatestContract("rocketAuctionManager", address(this)) { // Check lot exists and has not already had RPL recovered require(getLotExists(_lotIndex), "Lot does not exist"); require(!getLotRPLRecovered(_lotIndex), "Unclaimed RPL has already been recovered from the lot"); // Check RPL can be reclaimed from lot require(block.number >= getLotEndBlock(_lotIndex), "Lot bidding period has not concluded yet"); // Get & check remaining RPL amount uint256 remainingRplAmount = getLotRemainingRPLAmount(_lotIndex); require(remainingRplAmount > 0, "No unclaimed RPL is available to recover"); // Decrease allotted RPL balance & set RPL recovered status decreaseAllottedRPLBalance(remainingRplAmount); setLotRPLRecovered(_lotIndex, true); // Emit RPL recovered event emit RPLRecovered(_lotIndex, msg.sender, remainingRplAmount, block.timestamp); } } ================================================ FILE: contracts/contract/casper/compiled/Deposit.abi ================================================ [{"name": "DepositEvent", "inputs": [{"type": "bytes", "name": "pubkey", "indexed": false}, {"type": "bytes", "name": "withdrawal_credentials", "indexed": false}, {"type": "bytes", "name": "amount", "indexed": false}, {"type": "bytes", "name": "signature", "indexed": false}, {"type": "bytes", "name": "index", "indexed": false}], "anonymous": false, "type": "event"}, {"outputs": [], "inputs": [], "constant": false, "payable": false, "type": "constructor"}, {"name": "get_deposit_root", "outputs": [{"type": "bytes32", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 91674}, {"name": "get_deposit_count", "outputs": [{"type": "bytes", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 10433}, {"name": "deposit", "outputs": [], "inputs": [{"type": "bytes", "name": "pubkey"}, {"type": "bytes", "name": "withdrawal_credentials"}, {"type": "bytes", "name": "signature"}, {"type": "bytes32", "name": "deposit_data_root"}], "constant": false, "payable": true, "type": "function", "gas": 1334547}] ================================================ FILE: contracts/contract/dao/RocketDAOProposal.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/dao/RocketDAOProposalInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // A DAO proposal contract RocketDAOProposal is RocketBase, RocketDAOProposalInterface { using SafeMath for uint; // Events event ProposalAdded(address indexed proposer, string indexed proposalDAO, uint256 indexed proposalID, bytes payload, uint256 time); event ProposalVoted(uint256 indexed proposalID, address indexed voter, bool indexed supported, uint256 time); event ProposalExecuted(uint256 indexed proposalID, address indexed executor, uint256 time); event ProposalCancelled(uint256 indexed proposalID, address indexed canceller, uint256 time); // The namespace for any data stored in the trusted node DAO (do not change) string constant private daoProposalNameSpace = "dao.proposal."; // Only allow the DAO contract to access modifier onlyDAOContract(string memory _daoName) { // Load contracts require(keccak256(abi.encodePacked(getContractName(msg.sender))) == keccak256(abi.encodePacked(_daoName)), "Sender is not the required DAO contract"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 2; } /*** Proposals ****************/ // Get the current total proposals function getTotal() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "total"))); } // Get the DAO that this proposal belongs too function getDAO(uint256 _proposalID) override public view returns (string memory) { return getString(keccak256(abi.encodePacked(daoProposalNameSpace, "dao", _proposalID))); } // Get the member who proposed function getProposer(uint256 _proposalID) override public view returns (address) { return getAddress(keccak256(abi.encodePacked(daoProposalNameSpace, "proposer", _proposalID))); } // Get the proposal message function getMessage(uint256 _proposalID) override external view returns (string memory) { return getString(keccak256(abi.encodePacked(daoProposalNameSpace, "message", _proposalID))); } // Get the start block of this proposal function getStart(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "start", _proposalID))); } // Get the end block of this proposal function getEnd(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "end", _proposalID))); } // The block where the proposal expires and can no longer be executed if it is successful function getExpires(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "expires", _proposalID))); } // Get the created status of this proposal function getCreated(uint256 _proposalID) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "created", _proposalID))); } // Get the votes count for this proposal function getVotesFor(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", _proposalID))); } // Get the votes count against this proposal function getVotesAgainst(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", _proposalID))); } // How many votes are required for the proposal to succeed function getVotesRequired(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.required", _proposalID))); } // Get the cancelled status of this proposal function getCancelled(uint256 _proposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "cancelled", _proposalID))); } // Get the executed status of this proposal function getExecuted(uint256 _proposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "executed", _proposalID))); } // Get the votes against count of this proposal function getPayload(uint256 _proposalID) override public view returns (bytes memory) { return getBytes(keccak256(abi.encodePacked(daoProposalNameSpace, "payload", _proposalID))); } // Returns true if this proposal has already been voted on by a member function getReceiptHasVoted(uint256 _proposalID, address _nodeAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.hasVoted", _proposalID, _nodeAddress))); } // Returns true if this proposal was supported by this member function getReceiptSupported(uint256 _proposalID, address _nodeAddress) override external view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.supported", _proposalID, _nodeAddress))); } // Return the state of the specified proposal // A successful proposal can be executed immediately function getState(uint256 _proposalID) override public view returns (ProposalState) { // Check the proposal ID is legit require(getTotal() >= _proposalID && _proposalID > 0, "Invalid proposal ID"); // Get the amount of votes for and against uint256 votesFor = getVotesFor(_proposalID); uint256 votesAgainst = getVotesAgainst(_proposalID); // Now return the state of the current proposal if (getCancelled(_proposalID)) { // Cancelled by the proposer? return ProposalState.Cancelled; // Has it been executed? } else if (getExecuted(_proposalID)) { return ProposalState.Executed; // Is the proposal pending? Eg. waiting to be voted on } else if (block.timestamp < getStart(_proposalID)) { return ProposalState.Pending; // Vote was successful, is now awaiting execution } else if (votesFor >= getVotesRequired(_proposalID) && block.timestamp < getExpires(_proposalID)) { return ProposalState.Succeeded; // The proposal is active and can be voted on } else if (block.timestamp < getEnd(_proposalID)) { return ProposalState.Active; // Check the votes, was it defeated? } else if (votesFor <= votesAgainst || votesFor < getVotesRequired(_proposalID)) { return ProposalState.Defeated; } else { // Was it successful, but has now expired? and cannot be executed anymore? return ProposalState.Expired; } } // Add a proposal to the an RP DAO, immeditately becomes active // Calldata is passed as the payload to execute upon passing the proposal function add(address _member, string memory _dao, string memory _message, uint256 _startTime, uint256 _duration, uint256 _expires, uint256 _votesRequired, bytes memory _payload) override external onlyDAOContract(_dao) returns (uint256) { // Basic checks require(_startTime > block.timestamp, "Proposal start time must be in the future"); require(_duration > 0, "Proposal cannot have a duration of 0"); require(_expires > 0, "Proposal cannot have a execution expiration of 0"); require(_votesRequired > 0, "Proposal cannot have a 0 votes required to be successful"); // Set the end block uint256 endTime = _startTime.add(_duration); // Set the expires block uint256 expires = endTime.add(_expires); // Get the proposal ID uint256 proposalID = getTotal().add(1); // The data structure for a proposal setAddress(keccak256(abi.encodePacked(daoProposalNameSpace, "proposer", proposalID)), _member); // Which member is making the proposal setString(keccak256(abi.encodePacked(daoProposalNameSpace, "dao", proposalID)), _dao); // The DAO the proposal relates too setString(keccak256(abi.encodePacked(daoProposalNameSpace, "message", proposalID)), _message); // A general message that can be included with the proposal setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "start", proposalID)), _startTime); // The time the proposal becomes active for voting on setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "end", proposalID)), endTime); // The time the proposal where voting ends setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "expires", proposalID)), expires); // The time when the proposal expires and can no longer be executed if it is successful setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "created", proposalID)), block.timestamp); // The time the proposal was created at setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", proposalID)), 0); // Votes for this proposal setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", proposalID)), 0); // Votes against this proposal setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.required", proposalID)), _votesRequired); // How many votes are required for the proposal to pass setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "cancelled", proposalID)), false); // The proposer can cancel this proposal, but only before it passes setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "executed", proposalID)), false); // Has this proposals calldata been executed? setBytes(keccak256(abi.encodePacked(daoProposalNameSpace, "payload", proposalID)), _payload); // A calldata payload to execute after it is successful // Update the total proposals setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "total")), proposalID); // Log it emit ProposalAdded(_member, _dao, proposalID, _payload, block.timestamp); // Done return proposalID; } // Voting for or against a proposal function vote(address _member, uint256 _votes, uint256 _proposalID, bool _support) override external onlyDAOContract(getDAO(_proposalID)) { // Successful proposals can be executed immediately, add this as a check for people who are still trying to vote after it has passed require(getState(_proposalID) != ProposalState.Succeeded, "Proposal has passed, voting is complete and the proposal can now be executed"); // Check the proposal is in a state that can be voted on require(getState(_proposalID) == ProposalState.Active, "Voting is not active for this proposal"); // Has this member already voted on this proposal? require(!getReceiptHasVoted(_proposalID, _member), "Member has already voted on proposal"); // Add votes to proposal if(_support) { addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", _proposalID)), _votes); }else{ addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", _proposalID)), _votes); } // Record the vote receipt now setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.votes", _proposalID, _member)), _votes); setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.hasVoted", _proposalID, _member)), true); setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.supported", _proposalID, _member)), _support); // Log it emit ProposalVoted(_proposalID, _member, _support, block.timestamp); } // Execute a proposal if it has passed function execute(uint256 _proposalID) override external { // Firstly make sure this proposal has passed require(getState(_proposalID) == ProposalState.Succeeded, "Proposal has not succeeded, has expired or has already been executed"); // Set as executed now before running payload setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "executed", _proposalID)), true); // Ok all good, lets run the payload on the dao contract that the proposal relates too, it should execute one of the methods on this contract (bool success, bytes memory response) = getContractAddress(getDAO(_proposalID)).call(getPayload(_proposalID)); // Was there an error? require(success, getRevertMsg(response)); // Log it emit ProposalExecuted(_proposalID, tx.origin, block.timestamp); } // Cancel a proposal, can be cancelled by the original proposer only if it hasn't been executed yet function cancel(address _member, uint256 _proposalID) override external onlyDAOContract(getDAO(_proposalID)) { // Firstly make sure this proposal can be cancelled require(getState(_proposalID) == ProposalState.Pending || getState(_proposalID) == ProposalState.Active, "Proposal can only be cancelled if pending or active"); // Only allow the proposer to cancel require(getProposer(_proposalID) == _member, "Proposal can only be cancelled by the proposer"); // Set as cancelled now setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "cancelled", _proposalID)), true); // Log it emit ProposalCancelled(_proposalID, _member, block.timestamp); } } ================================================ FILE: contracts/contract/dao/node/RocketDAONodeTrusted.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../RocketBase.sol"; import "../../../interface/RocketVaultInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedProposalsInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedActionsInterface.sol"; import "../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMembersInterface.sol"; import "../../../interface/dao/RocketDAOProposalInterface.sol"; import "../../../interface/util/AddressSetStorageInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // The Trusted Node DAO contract RocketDAONodeTrusted is RocketBase, RocketDAONodeTrustedInterface { using SafeMath for uint; // The namespace for any data stored in the trusted node DAO (do not change) string constant daoNameSpace = "dao.trustednodes."; // Min amount of trusted node members required in the DAO uint256 constant daoMemberMinCount = 3; // Only allow bootstrapping when enabled modifier onlyBootstrapMode() { require(getBootstrapModeDisabled() == false, "Bootstrap mode not engaged"); _; } // Only when the DAO needs new members due to being below the required min modifier onlyLowMemberMode() { require(getMemberCount() < daoMemberMinCount, "Low member mode not engaged"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 1; } /**** DAO Properties **************/ // Returns true if bootstrap mode is disabled function getBootstrapModeDisabled() override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoNameSpace, "bootstrapmode.disabled"))); } /*** Proposals ****************/ // Return the amount of member votes need for a proposal to pass function getMemberQuorumVotesRequired() override external view returns (uint256) { // Load contracts RocketDAONodeTrustedSettingsMembersInterface rocketDAONodeTrustedSettingsMembers = RocketDAONodeTrustedSettingsMembersInterface(getContractAddress("rocketDAONodeTrustedSettingsMembers")); // Calculate and return votes required return getMemberCount().mul(rocketDAONodeTrustedSettingsMembers.getQuorum()); } /*** Members ******************/ // Return true if the node addressed passed is a member of the trusted node DAO function getMemberIsValid(address _nodeAddress) override external view returns (bool) { return getBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress))); } // Get a trusted node member address by index function getMemberAt(uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _index); } // Total number of members in the current trusted node DAO function getMemberCount() override public view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked(daoNameSpace, "member.index"))); } // Min required member count for the DAO function getMemberMinRequired() override external pure returns (uint256) { return daoMemberMinCount; } // Get the last time this user made a proposal function getMemberLastProposalTime(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.proposal.lasttime", _nodeAddress))); } // Get the ID of a trusted node member function getMemberID(address _nodeAddress) override external view returns (string memory) { return getString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _nodeAddress))); } // Get the URL of a trusted node member function getMemberUrl(address _nodeAddress) override external view returns (string memory) { return getString(keccak256(abi.encodePacked(daoNameSpace, "member.url", _nodeAddress))); } // Get the block the member joined at function getMemberJoinedTime(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress))); } // Get data that was recorded about a proposal that was executed function getMemberProposalExecutedTime(string memory _proposalType, address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", _proposalType, _nodeAddress))); } // Get the RPL bond amount the user deposited to join function getMemberRPLBondAmount(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.bond.rpl", _nodeAddress))); } // Is this member currently being 'challenged' to see if their node is responding function getMemberIsChallenged(address _nodeAddress) override external view returns (bool) { // Has this member been challenged recently and still within the challenge window to respond? If there is a challenge block recorded against them, they are actively being challenged. return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.challenged.time", _nodeAddress))) > 0 ? true : false; } // How many unbonded validators this member has function getMemberUnbondedValidatorCount(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.validator.unbonded.count", _nodeAddress))); } // Increment/decrement a member's unbonded validator count // Only accepts calls from the RocketMinipoolManager contract function incrementMemberUnbondedValidatorCount(address _nodeAddress) override external onlyLatestContract("rocketDAONodeTrusted", address(this)) onlyLatestContract("rocketMinipoolManager", msg.sender) { addUint(keccak256(abi.encodePacked(daoNameSpace, "member.validator.unbonded.count", _nodeAddress)), 1); } function decrementMemberUnbondedValidatorCount(address _nodeAddress) override external onlyLatestContract("rocketDAONodeTrusted", address(this)) onlyRegisteredMinipool(msg.sender) { subUint(keccak256(abi.encodePacked(daoNameSpace, "member.validator.unbonded.count", _nodeAddress)), 1); } /**** Bootstrapping ***************/ // Bootstrap mode - In bootstrap mode, guardian can add members at will function bootstrapMember(string memory _id, string memory _url, address _nodeAddress) override external onlyGuardian onlyBootstrapMode onlyRegisteredNode(_nodeAddress) onlyLatestContract("rocketDAONodeTrusted", address(this)) { // Ok good to go, lets add them RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalInvite(_id, _url, _nodeAddress); } // Bootstrap mode - Uint Setting function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) { // Ok good to go, lets update the settings RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalSettingUint(_settingContractName, _settingPath, _value); } // Bootstrap mode - Bool Setting function bootstrapSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) { // Ok good to go, lets update the settings RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalSettingBool(_settingContractName, _settingPath, _value); } // Bootstrap mode - Upgrade contracts or their ABI function bootstrapUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) { // Ok good to go, lets update the settings RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalUpgrade(_type, _name, _contractAbi, _contractAddress); } // Bootstrap mode - Disable RP Access (only RP can call this to hand over full control to the DAO) function bootstrapDisable(bool _confirmDisableBootstrapMode) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) { require(_confirmDisableBootstrapMode == true, "You must confirm disabling bootstrap mode, it can only be done once!"); setBool(keccak256(abi.encodePacked(daoNameSpace, "bootstrapmode.disabled")), true); } /**** Recovery ***************/ // In an explicable black swan scenario where the DAO loses more than the min membership required (3), this method can be used by a regular node operator to join the DAO // Must have their ID, URL, current RPL bond amount available and must be called by their current registered node account function memberJoinRequired(string memory _id, string memory _url) override external onlyLowMemberMode onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAONodeTrusted", address(this)) { // Ok good to go, lets update the settings RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalInvite(_id, _url, msg.sender); // Get the to automatically join as a member (by a regular proposal, they would have to manually accept, but this is no ordinary situation) RocketDAONodeTrustedActionsInterface(getContractAddress("rocketDAONodeTrustedActions")).actionJoinRequired(msg.sender); } } ================================================ FILE: contracts/contract/dao/node/RocketDAONodeTrustedActions.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../RocketBase.sol"; import "../../../interface/RocketVaultInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedActionsInterface.sol"; import "../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMembersInterface.sol"; import "../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsProposalsInterface.sol"; import "../../../interface/rewards/claims/RocketClaimTrustedNodeInterface.sol"; import "../../../interface/util/AddressSetStorageInterface.sol"; import "../../../interface/util/IERC20Burnable.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // The Trusted Node DAO Actions contract RocketDAONodeTrustedActions is RocketBase, RocketDAONodeTrustedActionsInterface { using SafeMath for uint; // Events event ActionJoined(address indexed nodeAddress, uint256 rplBondAmount, uint256 time); event ActionLeave(address indexed nodeAddress, uint256 rplBondAmount, uint256 time); event ActionKick(address indexed nodeAddress, uint256 rplBondAmount, uint256 time); event ActionChallengeMade(address indexed nodeChallengedAddress, address indexed nodeChallengerAddress, uint256 time); event ActionChallengeDecided(address indexed nodeChallengedAddress, address indexed nodeChallengeDeciderAddress, bool success, uint256 time); // The namespace for any data stored in the trusted node DAO (do not change) string constant private daoNameSpace = "dao.trustednodes."; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 2; } /*** Internal Methods **********************/ // Add a new member to the DAO function _memberAdd(address _nodeAddress, uint256 _rplBondAmountPaid) private onlyRegisteredNode(_nodeAddress) { // Load contracts RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Check current node status require(rocketDAONode.getMemberIsValid(_nodeAddress) != true, "This node is already part of the trusted node DAO"); // Flag them as a member now that they have accepted the invitation and record the size of the bond they paid setBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress)), true); // Add the bond amount they have paid if(_rplBondAmountPaid > 0) setUint(keccak256(abi.encodePacked(daoNameSpace, "member.bond.rpl", _nodeAddress)), _rplBondAmountPaid); // Record the block number they joined at setUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress)), block.timestamp); // Add to member index now addressSetStorage.addItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _nodeAddress); } // Remove a member from the DAO function _memberRemove(address _nodeAddress) private onlyTrustedNode(_nodeAddress) { // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Remove their membership now deleteBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress))); deleteAddress(keccak256(abi.encodePacked(daoNameSpace, "member.address", _nodeAddress))); deleteString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _nodeAddress))); deleteString(keccak256(abi.encodePacked(daoNameSpace, "member.url", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.bond.rpl", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.challenged.time", _nodeAddress))); // Clean up the invited/leave proposals deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "invited", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "leave", _nodeAddress))); // Remove from member index now addressSetStorage.removeItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _nodeAddress); } // A member official joins the DAO with their bond ready, if successful they are added as a member function _memberJoin(address _nodeAddress) private { // Set some intiial contract address address rocketVaultAddress = getContractAddress("rocketVault"); address rocketTokenRPLAddress = getContractAddress("rocketTokenRPL"); // Load contracts IERC20 rplInflationContract = IERC20(rocketTokenRPLAddress); RocketVaultInterface rocketVault = RocketVaultInterface(rocketVaultAddress); RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedSettingsMembersInterface rocketDAONodeTrustedSettingsMembers = RocketDAONodeTrustedSettingsMembersInterface(getContractAddress("rocketDAONodeTrustedSettingsMembers")); RocketDAONodeTrustedSettingsProposalsInterface rocketDAONodeTrustedSettingsProposals = RocketDAONodeTrustedSettingsProposalsInterface(getContractAddress("rocketDAONodeTrustedSettingsProposals")); // The time that the member was successfully invited to join the DAO uint256 memberInvitedTime = rocketDAONode.getMemberProposalExecutedTime("invited", _nodeAddress); // Have they been invited? require(memberInvitedTime > 0, "This node has not been invited to join"); // The current member bond amount in RPL that's required uint256 rplBondAmount = rocketDAONodeTrustedSettingsMembers.getRPLBond(); // Has their invite expired? require(memberInvitedTime.add(rocketDAONodeTrustedSettingsProposals.getActionTime()) > block.timestamp, "This node's invitation to join has expired, please apply again"); // Verify they have allowed this contract to spend their RPL for the bond require(rplInflationContract.allowance(_nodeAddress, address(this)) >= rplBondAmount, "Not enough allowance given to RocketDAONodeTrusted contract for transfer of RPL bond tokens"); // Transfer the tokens to this contract now require(rplInflationContract.transferFrom(_nodeAddress, address(this), rplBondAmount), "Token transfer to RocketDAONodeTrusted contract was not successful"); // Allow RocketVault to transfer these tokens to itself now require(rplInflationContract.approve(rocketVaultAddress, rplBondAmount), "Approval for RocketVault to spend RocketDAONodeTrusted RPL bond tokens was not successful"); // Let vault know it can move these tokens to itself now and credit the balance to this contract rocketVault.depositToken(getContractName(address(this)), IERC20(rocketTokenRPLAddress), rplBondAmount); // Add them as a member now that they have accepted the invitation and record the size of the bond they paid _memberAdd(_nodeAddress, rplBondAmount); // Log it emit ActionJoined(_nodeAddress, rplBondAmount, block.timestamp); } /*** Action Methods ************************/ // When a new member has been successfully invited to join, they must call this method to join officially // They will be required to have the RPL bond amount in their account // This method allows us to only allow them to join if they have a working node account and have been officially invited function actionJoin() override external onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedActions", address(this)) { _memberJoin(msg.sender); } // When the DAO has suffered a loss of members due to unforseen blackswan issue and has < the min required amount (3), a regular bonded node can directly join as a member and recover the DAO // They will be required to have the RPL bond amount in their account. This is called directly from RocketDAONodeTrusted. function actionJoinRequired(address _nodeAddress) override external onlyRegisteredNode(_nodeAddress) onlyLatestContract("rocketDAONodeTrusted", msg.sender) { _memberJoin(_nodeAddress); } // When a new member has successfully requested to leave with a proposal, they must call this method to leave officially and receive their RPL bond function actionLeave(address _rplBondRefundAddress) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedActions", address(this)) { // Load contracts RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedSettingsProposalsInterface rocketDAONodeTrustedSettingsProposals = RocketDAONodeTrustedSettingsProposalsInterface(getContractAddress("rocketDAONodeTrustedSettingsProposals")); // Check this wouldn't dip below the min required trusted nodes require(rocketDAONode.getMemberCount() > rocketDAONode.getMemberMinRequired(), "Member count will fall below min required"); // Get the time that they were approved to leave at uint256 leaveAcceptedTime = rocketDAONode.getMemberProposalExecutedTime("leave", msg.sender); // Has their leave request expired? require(leaveAcceptedTime.add(rocketDAONodeTrustedSettingsProposals.getActionTime()) > block.timestamp, "This member has not been approved to leave or request has expired, please apply to leave again"); // They were successful, lets refund their RPL Bond uint256 rplBondRefundAmount = rocketDAONode.getMemberRPLBondAmount(msg.sender); // Refund if(rplBondRefundAmount > 0) { // Valid withdrawal address require(_rplBondRefundAddress != address(0x0), "Member has not supplied a valid address for their RPL bond refund"); // Send tokens now rocketVault.withdrawToken(_rplBondRefundAddress, IERC20(getContractAddress("rocketTokenRPL")), rplBondRefundAmount); } // Remove them now _memberRemove(msg.sender); // Log it emit ActionLeave(msg.sender, rplBondRefundAmount, block.timestamp); } // A member can be evicted from the DAO by proposal, send their remaining RPL balance to them and remove from the DAO // Is run via the main DAO contract when the proposal passes and is executed function actionKick(address _nodeAddress, uint256 _rplFine) override external onlyTrustedNode(_nodeAddress) onlyLatestContract("rocketDAONodeTrustedProposals", msg.sender) { // Load contracts RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); IERC20 rplToken = IERC20(getContractAddress("rocketTokenRPL")); // Get the uint256 rplBondRefundAmount = rocketDAONode.getMemberRPLBondAmount(_nodeAddress); // Refund if (rplBondRefundAmount > 0) { // Send tokens now if the vault can cover it if(rplToken.balanceOf(address(rocketVault)) >= rplBondRefundAmount) rocketVault.withdrawToken(_nodeAddress, IERC20(getContractAddress("rocketTokenRPL")), rplBondRefundAmount); } // Burn the fine if (_rplFine > 0) { rocketVault.burnToken(IERC20Burnable(getContractAddress("rocketTokenRPL")), _rplFine); } // Remove the member now _memberRemove(_nodeAddress); // Log it emit ActionKick(_nodeAddress, rplBondRefundAmount, block.timestamp); } // In the event that the majority/all of members go offline permanently and no more proposals could be passed, a current member or a regular node can 'challenge' a DAO members node to respond // If it does not respond in the given window, it can be removed as a member. The one who removes the member after the challenge isn't met, must be another node other than the proposer to provide some oversight // This should only be used in an emergency situation to recover the DAO. Members that need removing when consensus is still viable, should be done via the 'kick' method. function actionChallengeMake(address _nodeAddress) override external onlyTrustedNode(_nodeAddress) onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedActions", address(this)) payable { // Load contracts RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedSettingsMembersInterface rocketDAONodeTrustedSettingsMembers = RocketDAONodeTrustedSettingsMembersInterface(getContractAddress("rocketDAONodeTrustedSettingsMembers")); // Members can challenge other members for free, but for a regular bonded node to challenge a DAO member, requires non-refundable payment to prevent spamming if(rocketDAONode.getMemberIsValid(msg.sender) != true) require(msg.value == rocketDAONodeTrustedSettingsMembers.getChallengeCost(), "Non DAO members must pay ETH to challenge a members node"); // Can't challenge yourself duh require(msg.sender != _nodeAddress, "You cannot challenge yourself"); // Is this member already being challenged? require(!rocketDAONode.getMemberIsChallenged(_nodeAddress), "Member is already being challenged"); // Has this node recently made another challenge and not waited for the cooldown to pass? require(getUint(keccak256(abi.encodePacked(daoNameSpace, "node.challenge.created.time", msg.sender))).add(rocketDAONodeTrustedSettingsMembers.getChallengeCooldown()) < block.timestamp, "You must wait for the challenge cooldown to pass before issuing another challenge"); // Ok challenge accepted // Record the last time this member challenged setUint(keccak256(abi.encodePacked(daoNameSpace, "node.challenge.created.time", msg.sender)), block.timestamp); // Record the challenge block now setUint(keccak256(abi.encodePacked(daoNameSpace, "member.challenged.time", _nodeAddress)), block.timestamp); // Record who made the challenge setAddress(keccak256(abi.encodePacked(daoNameSpace, "member.challenged.by", _nodeAddress)), msg.sender); // Log it emit ActionChallengeMade(_nodeAddress, msg.sender, block.timestamp); } // Decides the success of a challenge. If called by the challenged node within the challenge window, the challenge is defeated and the member stays as they have indicated their node is still alive. // If called after the challenge window has passed by anyone except the original challenge initiator, then the challenge has succeeded and the member is removed function actionChallengeDecide(address _nodeAddress) override external onlyTrustedNode(_nodeAddress) onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedActions", address(this)) { // Load contracts RocketDAONodeTrustedSettingsMembersInterface rocketDAONodeTrustedSettingsMembers = RocketDAONodeTrustedSettingsMembersInterface(getContractAddress("rocketDAONodeTrustedSettingsMembers")); // Was the challenge successful? bool challengeSuccess = false; // Get the block the challenge was initiated at bytes32 challengeTimeKey = keccak256(abi.encodePacked(daoNameSpace, "member.challenged.time", _nodeAddress)); uint256 challengeTime = getUint(challengeTimeKey); // If challenge time is 0, the member hasn't been challenged or they have successfully responded to the challenge previously require(challengeTime > 0, "Member hasn't been challenged or they have successfully responded to the challenge already"); // Allow the challenged member to refute the challenge at anytime. If the window has passed and the challenge node does not run this method, any member can decide the challenge and eject the absent member // Is it the node being challenged? if(_nodeAddress == msg.sender) { // Challenge is defeated, node has responded deleteUint(challengeTimeKey); }else{ // The challenge refute window has passed, the member can be ejected now require(challengeTime.add(rocketDAONodeTrustedSettingsMembers.getChallengeWindow()) < block.timestamp, "Refute window has not yet passed"); // Node has been challenged and failed to respond in the given window, remove them as a member and their bond is burned _memberRemove(_nodeAddress); // Challenge was successful challengeSuccess = true; } // Log it emit ActionChallengeDecided(_nodeAddress, msg.sender, challengeSuccess, block.timestamp); } } ================================================ FILE: contracts/contract/dao/node/RocketDAONodeTrustedProposals.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../RocketBase.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedProposalsInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedActionsInterface.sol"; import "../../../interface/dao/node/RocketDAONodeTrustedUpgradeInterface.sol"; import "../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsInterface.sol"; import "../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsProposalsInterface.sol"; import "../../../interface/dao/RocketDAOProposalInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // The Trusted Node DAO Proposals contract RocketDAONodeTrustedProposals is RocketBase, RocketDAONodeTrustedProposalsInterface { using SafeMath for uint; // The namespace for any data stored in the trusted node DAO (do not change) string constant daoNameSpace = "dao.trustednodes."; // Only allow certain contracts to execute methods modifier onlyExecutingContracts() { // Methods are either executed by bootstrapping methods in rocketDAONodeTrusted or by people executing passed proposals in rocketDAOProposal require(msg.sender == getContractAddress("rocketDAONodeTrusted") || msg.sender == getContractAddress("rocketDAOProposal"), "Sender is not permitted to access executing methods"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 1; } /*** Proposals **********************/ // Create a DAO proposal with calldata, if successful will be added to a queue where it can be executed // A general message can be passed by the proposer along with the calldata payload that can be executed if the proposal passes function propose(string memory _proposalMessage, bytes memory _payload) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedProposals", address(this)) returns (uint256) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedSettingsProposalsInterface rocketDAONodeTrustedSettingsProposals = RocketDAONodeTrustedSettingsProposalsInterface(getContractAddress("rocketDAONodeTrustedSettingsProposals")); // Check this user can make a proposal now require(daoNodeTrusted.getMemberLastProposalTime(msg.sender).add(rocketDAONodeTrustedSettingsProposals.getCooldownTime()) <= block.timestamp, "Member has not waited long enough to make another proposal"); // Require the min amount of members are in to make a proposal require(daoNodeTrusted.getMemberCount() >= daoNodeTrusted.getMemberMinRequired(), "Min member count not met to allow proposals to be added"); // Record the last time this user made a proposal setUint(keccak256(abi.encodePacked(daoNameSpace, "member.proposal.lasttime", msg.sender)), block.timestamp); // Create the proposal return daoProposal.add(msg.sender, "rocketDAONodeTrustedProposals", _proposalMessage, block.timestamp.add(rocketDAONodeTrustedSettingsProposals.getVoteDelayTime()), rocketDAONodeTrustedSettingsProposals.getVoteTime(), rocketDAONodeTrustedSettingsProposals.getExecuteTime(), daoNodeTrusted.getMemberQuorumVotesRequired(), _payload); } // Vote on a proposal function vote(uint256 _proposalID, bool _support) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); // Did they join after this proposal was created? If so, they can't vote or it'll throw off the set proposalVotesRequired require(daoNodeTrusted.getMemberJoinedTime(msg.sender) < daoProposal.getCreated(_proposalID), "Member cannot vote on proposal created before they became a member"); // Vote now, one vote per trusted node member daoProposal.vote(msg.sender, 1 ether, _proposalID, _support); } // Cancel a proposal function cancel(uint256 _proposalID) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketDAONodeTrustedProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Cancel now, will succeed if it is the original proposer daoProposal.cancel(msg.sender, _proposalID); } // Execute a proposal function execute(uint256 _proposalID) override external onlyLatestContract("rocketDAONodeTrustedProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Execute now daoProposal.execute(_proposalID); } /*** Proposal - Members **********************/ // A new DAO member being invited, can only be done via a proposal or in bootstrap mode // Provide an ID that indicates who is running the trusted node and the address of the registered node that they wish to propose joining the dao function proposalInvite(string memory _id, string memory _url, address _nodeAddress) override external onlyExecutingContracts onlyRegisteredNode(_nodeAddress) { // Their proposal executed, record the block setUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "invited", _nodeAddress)), block.timestamp); // Ok all good, lets get their invitation and member data setup // They are initially only invited to join, so their membership isn't set as true until they accept it in RocketDAONodeTrustedActions _memberInit(_id, _url, _nodeAddress); } // A current member proposes leaving the trusted node DAO, when successful they will be allowed to collect their RPL bond function proposalLeave(address _nodeAddress) override external onlyExecutingContracts onlyTrustedNode(_nodeAddress) { // Load contracts RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); // Check this wouldn't dip below the min required trusted nodes (also checked when the node has a successful proposal and attempts to exit) require(daoNodeTrusted.getMemberCount() > daoNodeTrusted.getMemberMinRequired(), "Member count will fall below min required"); // Their proposal to leave has been accepted, record the block setUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "leave", _nodeAddress)), block.timestamp); } // Propose to kick a current member from the DAO with an optional RPL bond fine function proposalKick(address _nodeAddress, uint256 _rplFine) override external onlyExecutingContracts onlyTrustedNode(_nodeAddress) { // Load contracts RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedActionsInterface daoActionsContract = RocketDAONodeTrustedActionsInterface(getContractAddress("rocketDAONodeTrustedActions")); // How much is their RPL bond? uint256 rplBondAmount = daoNodeTrusted.getMemberRPLBondAmount(_nodeAddress); // Check fine amount can be covered require(_rplFine <= rplBondAmount, "RPL Fine must be lower or equal to the RPL bond amount of the node being kicked"); // Set their bond amount minus the fine setUint(keccak256(abi.encodePacked(daoNameSpace, "member.bond.rpl", _nodeAddress)), rplBondAmount.sub(_rplFine)); // Kick them now daoActionsContract.actionKick(_nodeAddress, _rplFine); } /*** Proposal - Settings ***************/ // Change one of the current uint256 settings of the DAO function proposalSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override external onlyExecutingContracts() { // Load contracts RocketDAONodeTrustedSettingsInterface rocketDAONodeTrustedSettings = RocketDAONodeTrustedSettingsInterface(getContractAddress(_settingContractName)); // Lets update rocketDAONodeTrustedSettings.setSettingUint(_settingPath, _value); } // Change one of the current bool settings of the DAO function proposalSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) override external onlyExecutingContracts() { // Load contracts RocketDAONodeTrustedSettingsInterface rocketDAONodeTrustedSettings = RocketDAONodeTrustedSettingsInterface(getContractAddress(_settingContractName)); // Lets update rocketDAONodeTrustedSettings.setSettingBool(_settingPath, _value); } /*** Proposal - Upgrades ***************/ // Upgrade contracts or ABI's if the DAO agrees function proposalUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) override external onlyExecutingContracts() { // Load contracts RocketDAONodeTrustedUpgradeInterface rocketDAONodeTrustedUpgradeInterface = RocketDAONodeTrustedUpgradeInterface(getContractAddress("rocketDAONodeTrustedUpgrade")); // Lets update rocketDAONodeTrustedUpgradeInterface.upgrade(_type, _name, _contractAbi, _contractAddress); } /*** Internal ***************/ // Add a new potential members data, they are not official members yet, just propsective function _memberInit(string memory _id, string memory _url, address _nodeAddress) private onlyRegisteredNode(_nodeAddress) { // Load contracts RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); // Check current node status require(!daoNodeTrusted.getMemberIsValid(_nodeAddress), "This node is already part of the trusted node DAO"); // Verify the ID is min 3 chars require(bytes(_id).length >= 3, "The ID for this new member must be at least 3 characters"); // Check URL length require(bytes(_url).length >= 6, "The URL for this new member must be at least 6 characters"); // Member initial data, not official until the bool is flagged as true setBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress)), false); setAddress(keccak256(abi.encodePacked(daoNameSpace, "member.address", _nodeAddress)), _nodeAddress); setString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _nodeAddress)), _id); setString(keccak256(abi.encodePacked(daoNameSpace, "member.url", _nodeAddress)), _url); setUint(keccak256(abi.encodePacked(daoNameSpace, "member.bond.rpl", _nodeAddress)), 0); setUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress)), 0); } } ================================================ FILE: contracts/contract/dao/node/RocketDAONodeTrustedUpgrade.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../interface/RocketStorageInterface.sol"; import {RocketDAONodeTrustedUpgradeInterface} from "../../../interface/dao/node/RocketDAONodeTrustedUpgradeInterface.sol"; import {RocketDAOProtocolSettingsSecurityInterface} from "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; import {RocketBase} from "../../RocketBase.sol"; /// @notice Handles network contract upgrades contract RocketDAONodeTrustedUpgrade is RocketBase, RocketDAONodeTrustedUpgradeInterface { // Events event UpgradePending(uint256 upgradeProposalID, bytes32 indexed upgradeType, bytes32 indexed name, uint256 time); event UpgradeVetoed(uint256 upgradeProposalID, uint256 time); event ContractUpgraded(bytes32 indexed name, address indexed oldAddress, address indexed newAddress, uint256 time); event ContractAdded(bytes32 indexed name, address indexed newAddress, uint256 time); event ABIUpgraded(bytes32 indexed name, uint256 time); event ABIAdded(bytes32 indexed name, uint256 time); // The namespace for any storage data used by this contract string constant private daoUpgradeNameSpace = "dao.upgrade."; // Immutables bytes32 immutable internal daoTrustedBootstrapKey; bytes32 immutable internal typeUpgradeContract; bytes32 immutable internal typeAddContract; bytes32 immutable internal typeUpgradeABI; bytes32 immutable internal typeAddABI; // Only allow bootstrapping when enabled modifier onlyBootstrapMode() { require(getBool(daoTrustedBootstrapKey) == false, "Bootstrap mode not engaged"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; // Precompute keys typeUpgradeContract = keccak256(abi.encodePacked("upgradeContract")); typeAddContract = keccak256(abi.encodePacked("addContract")); typeUpgradeABI = keccak256(abi.encodePacked("upgradeABI")); typeAddABI = keccak256(abi.encodePacked("addABI")); daoTrustedBootstrapKey = keccak256(abi.encodePacked("dao.trustednodes.", "bootstrapmode.disabled")); } /// @notice Called when an upgrade proposal is executed, creates an upgrade proposal that can be vetoed by the /// security council or executed after the upgrade delay period has passed /// @param _type Type of upgrade (valid values: "upgradeContract", "addContract", "upgradeABI", "addABI") /// @param _name Contract name to upgrade /// @param _contractAbi ABI of the upgraded contract /// @param _contractAddress Address of the upgraded contract function upgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) override external onlyLatestContract("rocketDAONodeTrustedProposals", msg.sender) { uint256 upgradeProposalID = getTotal() + 1; // Compute when the proposal can be executed if not vetoed by the security council uint256 startTime = block.timestamp; RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); uint256 endTime = startTime + rocketDAOProtocolSettingsSecurity.getUpgradeDelay(); // Store data bytes32 typeHash = keccak256(abi.encodePacked(_type)); setBytes32(keccak256(abi.encodePacked(daoUpgradeNameSpace, "type", upgradeProposalID)), typeHash); setString(keccak256(abi.encodePacked(daoUpgradeNameSpace, "name", upgradeProposalID)), _name); setString(keccak256(abi.encodePacked(daoUpgradeNameSpace, "abi", upgradeProposalID)), _contractAbi); setAddress(keccak256(abi.encodePacked(daoUpgradeNameSpace, "address", upgradeProposalID)), _contractAddress); setUint(keccak256(abi.encodePacked(daoUpgradeNameSpace, "end", upgradeProposalID)), endTime); addUint(keccak256(abi.encodePacked(daoUpgradeNameSpace, "total")), 1); // Emit event emit UpgradePending(upgradeProposalID, typeHash, keccak256(abi.encodePacked(_name)), block.timestamp); } /// @notice Called by the proposal contract when a veto passes /// @param _upgradeProposalID ID of the upgrade proposal to veto function veto(uint256 _upgradeProposalID) override external onlyLatestContract("rocketDAOSecurityUpgrade", msg.sender) { // Validate proposal state require(getState(_upgradeProposalID) == UpgradeProposalState.Pending, "Proposal has already succeeded, expired, or executed"); // Mark the upgrade as vetoed setBool(keccak256(abi.encodePacked(daoUpgradeNameSpace, "vetoed", _upgradeProposalID)), true); // Emit event emit UpgradeVetoed(_upgradeProposalID, block.timestamp); } /// @notice Called after upgrade delay has passed to perform the upgrade /// @param _upgradeProposalID ID of the upgrade proposal to execute /// @dev Must be called by a registered trusted node function execute(uint256 _upgradeProposalID) override external onlyTrustedNode(msg.sender) { // Validate proposal state require(getState(_upgradeProposalID) == UpgradeProposalState.Succeeded, "Proposal has not succeeded or has been vetoed or executed"); // Mark as executed setBool(keccak256(abi.encodePacked(daoUpgradeNameSpace, "executed", _upgradeProposalID)), true); // Execute the upgrade _execute(getType(_upgradeProposalID), getName(_upgradeProposalID), getUpgradeABI(_upgradeProposalID), getUpgradeAddress(_upgradeProposalID)); } /// @notice Immediately execute an upgrade if bootstrap mode is still enabled /// @param _type Type of upgrade (valid values: "upgradeContract", "addContract", "upgradeABI", "addABI") /// @param _name Contract name to upgrade /// @param _contractAbi ABI of the upgraded contract /// @param _contractAddress Address of the upgraded contract function bootstrapUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) override external onlyGuardian onlyBootstrapMode { bytes32 typeHash = keccak256(abi.encodePacked(_type)); _execute(typeHash, _name, _contractAbi, _contractAddress); } /// @dev Internal implementation of the execution process function _execute(bytes32 _typeHash, string memory _name, string memory _contractAbi, address _contractAddress) internal { if (_typeHash == typeUpgradeContract) _upgradeContract(_name, _contractAddress, _contractAbi); else if (_typeHash == typeAddContract) _addContract(_name, _contractAddress, _contractAbi); else if (_typeHash == typeUpgradeABI) _upgradeABI(_name, _contractAbi); else if (_typeHash == typeAddABI) _addABI(_name, _contractAbi); else revert("Invalid upgrade type"); } /// @dev Performs an update to a contract and ABI simultaneously function _upgradeContract(string memory _name, address _contractAddress, string memory _contractAbi) internal { // Check contract being upgraded bytes32 nameHash = keccak256(abi.encodePacked(_name)); require(nameHash != keccak256(abi.encodePacked("rocketVault")), "Cannot upgrade the vault"); require(nameHash != keccak256(abi.encodePacked("rocketTokenRETH")), "Cannot upgrade token contracts"); require(nameHash != keccak256(abi.encodePacked("rocketTokenRPL")), "Cannot upgrade token contracts"); require(nameHash != keccak256(abi.encodePacked("rocketTokenRPLFixedSupply")), "Cannot upgrade token contracts"); require(nameHash != keccak256(abi.encodePacked("casperDeposit")), "Cannot upgrade the casper deposit contract"); require(nameHash != keccak256(abi.encodePacked("rocketMinipoolPenalty")), "Cannot upgrade minipool penalty contract"); // Get old contract address & check contract exists address oldContractAddress = getAddress(keccak256(abi.encodePacked("contract.address", _name))); require(oldContractAddress != address(0x0), "Contract does not exist"); // Check new contract address require(_contractAddress != address(0x0), "Invalid contract address"); require(_contractAddress != oldContractAddress, "The contract address cannot be set to its current address"); require(!getBool(keccak256(abi.encodePacked("contract.exists", _contractAddress))), "Contract address is already in use"); // Check ABI isn't empty require(bytes(_contractAbi).length > 0, "Empty ABI is invalid"); // Register new contract setBool(keccak256(abi.encodePacked("contract.exists", _contractAddress)), true); setString(keccak256(abi.encodePacked("contract.name", _contractAddress)), _name); setAddress(keccak256(abi.encodePacked("contract.address", _name)), _contractAddress); setString(keccak256(abi.encodePacked("contract.abi", _name)), _contractAbi); // Deregister old contract deleteString(keccak256(abi.encodePacked("contract.name", oldContractAddress))); deleteBool(keccak256(abi.encodePacked("contract.exists", oldContractAddress))); // Emit contract upgraded event emit ContractUpgraded(nameHash, oldContractAddress, _contractAddress, block.timestamp); } /// @dev Adds a new contract to the protocol function _addContract(string memory _name, address _contractAddress, string memory _contractAbi) internal { // Check contract name bytes32 nameHash = keccak256(abi.encodePacked(_name)); require(bytes(_name).length > 0, "Invalid contract name"); // Cannot add contract if it already exists (use upgradeContract instead) require(getAddress(keccak256(abi.encodePacked("contract.address", _name))) == address(0x0), "Contract name is already in use"); // Cannot add contract if already in use as ABI only string memory existingAbi = getString(keccak256(abi.encodePacked("contract.abi", _name))); require(bytes(existingAbi).length == 0, "Contract name is already in use"); // Check contract address require(_contractAddress != address(0x0), "Invalid contract address"); require(!getBool(keccak256(abi.encodePacked("contract.exists", _contractAddress))), "Contract address is already in use"); // Check ABI isn't empty require(bytes(_contractAbi).length > 0, "Empty ABI is invalid"); // Register contract setBool(keccak256(abi.encodePacked("contract.exists", _contractAddress)), true); setString(keccak256(abi.encodePacked("contract.name", _contractAddress)), _name); setAddress(keccak256(abi.encodePacked("contract.address", _name)), _contractAddress); setString(keccak256(abi.encodePacked("contract.abi", _name)), _contractAbi); // Emit contract added event emit ContractAdded(nameHash, _contractAddress, block.timestamp); } /// @dev Upgrades an existing ABI function _upgradeABI(string memory _name, string memory _contractAbi) internal { // Check ABI exists string memory existingAbi = getString(keccak256(abi.encodePacked("contract.abi", _name))); require(bytes(existingAbi).length > 0, "ABI does not exist"); // Sanity checks require(bytes(_contractAbi).length > 0, "Empty ABI is invalid"); require(keccak256(bytes(existingAbi)) != keccak256(bytes(_contractAbi)), "ABIs are identical"); // Set ABI setString(keccak256(abi.encodePacked("contract.abi", _name)), _contractAbi); // Emit ABI upgraded event emit ABIUpgraded(keccak256(abi.encodePacked(_name)), block.timestamp); } /// @dev Adds a new ABI to the protocol function _addABI(string memory _name, string memory _contractAbi) internal { // Check ABI name bytes32 nameHash = keccak256(abi.encodePacked(_name)); require(bytes(_name).length > 0, "Invalid ABI name"); // Sanity check require(bytes(_contractAbi).length > 0, "Empty ABI is invalid"); // Cannot add ABI if name is already used for an existing network contract require(getAddress(keccak256(abi.encodePacked("contract.address", _name))) == address(0x0), "ABI name is already in use"); // Cannot add ABI if ABI already exists for this name (use upgradeABI instead) string memory existingAbi = getString(keccak256(abi.encodePacked("contract.abi", _name))); require(bytes(existingAbi).length == 0, "ABI name is already in use"); // Set ABI setString(keccak256(abi.encodePacked("contract.abi", _name)), _contractAbi); // Emit ABI added event emit ABIAdded(nameHash, block.timestamp); } /// @notice Get the total number of upgrade proposals function getTotal() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoUpgradeNameSpace, "total"))); } /// @notice Return the state of the specified upgrade proposal /// @param _upgradeProposalID ID of the upgrade proposal to query function getState(uint256 _upgradeProposalID) override public view returns (UpgradeProposalState) { // Check the proposal ID is legit require(getTotal() >= _upgradeProposalID && _upgradeProposalID > 0, "Invalid upgrade proposal ID"); if (getVetoed(_upgradeProposalID)) { return UpgradeProposalState.Vetoed; } else if (getExecuted(_upgradeProposalID)) { return UpgradeProposalState.Executed; } else if (block.timestamp < getEnd(_upgradeProposalID)) { return UpgradeProposalState.Pending; } else { return UpgradeProposalState.Succeeded; } } /// @notice Get the end time of this proposal (when the upgrade delay ends) /// @param _upgradeProposalID ID of the upgrade proposal to query function getEnd(uint256 _upgradeProposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoUpgradeNameSpace, "end", _upgradeProposalID))); } /// @notice Get whether the proposal has been executed /// @param _upgradeProposalID ID of the upgrade proposal to query function getExecuted(uint256 _upgradeProposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoUpgradeNameSpace, "executed", _upgradeProposalID))); } /// @notice Get whether the proposal has been vetoed /// @param _upgradeProposalID ID of the upgrade proposal to query function getVetoed(uint256 _upgradeProposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoUpgradeNameSpace, "vetoed", _upgradeProposalID))); } /// @notice Get the proposal type /// @param _upgradeProposalID ID of the upgrade proposal to query function getType(uint256 _upgradeProposalID) override public view returns (bytes32) { return getBytes32(keccak256(abi.encodePacked(daoUpgradeNameSpace, "type", _upgradeProposalID))); } /// @notice Get the proposed upgrade contract name /// @param _upgradeProposalID ID of the upgrade proposal to query function getName(uint256 _upgradeProposalID) override public view returns (string memory) { return getString(keccak256(abi.encodePacked(daoUpgradeNameSpace, "name", _upgradeProposalID))); } /// @notice Get the proposed upgrade contract address /// @param _upgradeProposalID ID of the upgrade proposal to query function getUpgradeAddress(uint256 _upgradeProposalID) override public view returns (address) { return getAddress(keccak256(abi.encodePacked(daoUpgradeNameSpace, "address", _upgradeProposalID))); } /// @notice Get the proposed upgrade contract ABI /// @param _upgradeProposalID ID of the upgrade proposal to query function getUpgradeABI(uint256 _upgradeProposalID) override public view returns (string memory) { return getString(keccak256(abi.encodePacked(daoUpgradeNameSpace, "abi", _upgradeProposalID))); } } ================================================ FILE: contracts/contract/dao/node/settings/RocketDAONodeTrustedSettings.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../../RocketBase.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsInterface.sol"; // Settings in RP which the DAO will have full control over // This settings contract enables storage using setting paths with namespaces, rather than explicit set methods abstract contract RocketDAONodeTrustedSettings is RocketBase, RocketDAONodeTrustedSettingsInterface { // The namespace for a particular group of settings bytes32 settingNameSpace; // Only allow updating from the DAO proposals contract modifier onlyDAONodeTrustedProposal() { // If this contract has been initialised, only allow access from the proposals contract if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) require(getContractAddress("rocketDAONodeTrustedProposals") == msg.sender, "Only DAO Node Trusted Proposals contract can update a setting"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress, string memory _settingNameSpace) RocketBase(_rocketStorageAddress) { // Apply the setting namespace settingNameSpace = keccak256(abi.encodePacked("dao.trustednodes.setting.", _settingNameSpace)); } /*** Uints ****************/ // A general method to return any setting given the setting path is correct, only accepts uints function getSettingUint(string memory _settingPath) public view override returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); } // Update a Uint setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingUint(string memory _settingPath, uint256 _value) virtual public override onlyDAONodeTrustedProposal { // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /*** Bools ****************/ // A general method to return any setting given the setting path is correct, only accepts bools function getSettingBool(string memory _settingPath) public view override returns (bool) { return getBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); } // Update a setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingBool(string memory _settingPath, bool _value) virtual public override onlyDAONodeTrustedProposal { // Update setting now setBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } } ================================================ FILE: contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsMembers.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "./RocketDAONodeTrustedSettings.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMembersInterface.sol"; // The Trusted Node DAO Members contract RocketDAONodeTrustedSettingsMembers is RocketDAONodeTrustedSettings, RocketDAONodeTrustedSettingsMembersInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAONodeTrustedSettings(_rocketStorageAddress, "members") { // Set version version = 1; // Initialize settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings setSettingUint("members.quorum", 0.51 ether); // Member quorum threshold that must be met for proposals to pass (51%) setSettingUint("members.rplbond", 1750 ether); // Bond amount required for a new member to join (in RPL) setSettingUint("members.minipool.unbonded.max", 30); // The amount of unbonded minipool validators members can make (these validators are only used if no regular bonded validators are available) setSettingUint("members.minipool.unbonded.min.fee", 0.8 ether); // Node fee must be over this percentage of the maximum fee before validator members are allowed to make unbonded pools (80%) setSettingUint("members.challenge.cooldown", 7 days); // How long a member must wait before performing another challenge in seconds setSettingUint("members.challenge.window", 7 days); // How long a member has to respond to a challenge in seconds setSettingUint("members.challenge.cost", 1 ether); // How much it costs a non-member to challenge a members node. It's free for current members to challenge other members. // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /*** Set Uint *****************************************/ // Update a setting, overrides inherited setting method with extra checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAONodeTrustedProposal { // Some safety guards for certain settings if(keccak256(abi.encodePacked(_settingPath)) == keccak256(abi.encodePacked("members.quorum"))) require(_value > 0 ether && _value <= 0.9 ether, "Quorum setting must be > 0 & <= 90%"); // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } // Getters // The member proposal quorum threshold for this DAO function getQuorum() override external view returns (uint256) { return getSettingUint("members.quorum"); } // Amount of RPL needed for a new member function getRPLBond() override external view returns (uint256) { return getSettingUint("members.rplbond"); } // The amount of unbonded minipool validators members can make (these validators are only used if no regular bonded validators are available) function getMinipoolUnbondedMax() override external view returns (uint256) { return getSettingUint("members.minipool.unbonded.max"); } // Node fee must be over this percentage of the maximum fee before validator members are allowed to make unbonded pools function getMinipoolUnbondedMinFee() override external view returns (uint256) { return getSettingUint('members.minipool.unbonded.min.fee'); } // How long a member must wait before making consecutive challenges in seconds function getChallengeCooldown() override external view returns (uint256) { return getSettingUint("members.challenge.cooldown"); } // The window available to meet any node challenges in seconds function getChallengeWindow() override external view returns (uint256) { return getSettingUint("members.challenge.window"); } // How much it costs a non-member to challenge a members node. It's free for current members to challenge other members. function getChallengeCost() override external view returns (uint256) { return getSettingUint("members.challenge.cost"); } } ================================================ FILE: contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsMinipool.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "./RocketDAONodeTrustedSettings.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; /// @notice The Trusted Node DAO Minipool settings contract RocketDAONodeTrustedSettingsMinipool is RocketDAONodeTrustedSettings, RocketDAONodeTrustedSettingsMinipoolInterface { using SafeMath for uint; constructor(RocketStorageInterface _rocketStorageAddress) RocketDAONodeTrustedSettings(_rocketStorageAddress, "minipool") { // Set version version = 2; // If deployed during initial deployment, initialise now (otherwise must be called after upgrade) if (!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings setSettingUint("minipool.scrub.period", 12 hours); setSettingUint("minipool.promotion.scrub.period", 3 days); setSettingUint("minipool.scrub.quorum", 0.51 ether); setSettingBool("minipool.scrub.penalty.enabled", false); setSettingUint("minipool.bond.reduction.window.start", 2 days); setSettingUint("minipool.bond.reduction.window.length", 2 days); setSettingUint("minipool.cancel.bond.reduction.quorum", 0.51 ether); // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract /// @param _settingPath The path of the setting within this contract's namespace /// @param _value The value to set it to function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAONodeTrustedProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { if(keccak256(abi.encodePacked(_settingPath)) == keccak256(abi.encodePacked("minipool.scrub.period"))) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); require(_value <= (rocketDAOProtocolSettingsMinipool.getLaunchTimeout().sub(1 hours)), "Scrub period must be less than launch timeout"); } } // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } // Getters /// @notice How long minipools must wait before moving to staking status (can be scrubbed by ODAO before then) function getScrubPeriod() override external view returns (uint256) { return getSettingUint("minipool.scrub.period"); } /// @notice How long minipools must wait before promoting a vacant minipool to staking status (can be scrubbed by ODAO before then) function getPromotionScrubPeriod() override external view returns (uint256) { return getSettingUint("minipool.promotion.scrub.period"); } /// @notice Returns the required number of trusted nodes to vote to scrub a minipool function getScrubQuorum() override external view returns (uint256) { return getSettingUint("minipool.scrub.quorum"); } /// @notice Returns the required number of trusted nodes to vote to cancel a bond reduction function getCancelBondReductionQuorum() override external view returns (uint256) { return getSettingUint("minipool.cancel.bond.reduction.quorum"); } /// @notice Returns true if scrubbing results in an RPL penalty for the node operator function getScrubPenaltyEnabled() override external view returns (bool) { return getSettingBool("minipool.scrub.penalty.enabled"); } /// @notice Returns true if the given time is within the bond reduction window function isWithinBondReductionWindow(uint256 _time) override external view returns (bool) { uint256 start = getBondReductionWindowStart(); uint256 length = getBondReductionWindowLength(); return (_time >= start && _time < (start.add(length))); } /// @notice Returns the start of the bond reduction window function getBondReductionWindowStart() override public view returns (uint256) { return getSettingUint("minipool.bond.reduction.window.start"); } /// @notice Returns the length of the bond reduction window function getBondReductionWindowLength() override public view returns (uint256) { return getSettingUint("minipool.bond.reduction.window.length"); } } ================================================ FILE: contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsProposals.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "./RocketDAONodeTrustedSettings.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsProposalsInterface.sol"; // The Trusted Node DAO Members contract RocketDAONodeTrustedSettingsProposals is RocketDAONodeTrustedSettings, RocketDAONodeTrustedSettingsProposalsInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAONodeTrustedSettings(_rocketStorageAddress, "proposals") { // Set version version = 1; // Initialize settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings setSettingUint("proposal.cooldown.time", 2 days); // How long before a member can make sequential proposals setSettingUint("proposal.vote.time", 2 weeks); // How long a proposal can be voted on setSettingUint("proposal.vote.delay.time", 1 weeks); // How long before a proposal can be voted on after it is created setSettingUint("proposal.execute.time", 4 weeks); // How long a proposal can be executed after its voting period is finished setSettingUint("proposal.action.time", 4 weeks); // Certain proposals require a secondary action to be run after the proposal is successful (joining, leaving etc). This is how long until that action expires // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } // Getters // How long before a member can make sequential proposals function getCooldownTime() override external view returns (uint256) { return getSettingUint("proposal.cooldown.time"); } // How long a proposal can be voted on function getVoteTime() override external view returns (uint256) { return getSettingUint("proposal.vote.time"); } // How long before a proposal can be voted on after it is created function getVoteDelayTime() override external view returns (uint256) { return getSettingUint("proposal.vote.delay.time"); } // How long a proposal can be executed after its voting period is finished function getExecuteTime() override external view returns (uint256) { return getSettingUint("proposal.execute.time"); } // Certain proposals require a secondary action to be run after the proposal is successful (joining, leaving etc). This is how long until that action expires function getActionTime() override external view returns (uint256) { return getSettingUint("proposal.action.time"); } } ================================================ FILE: contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsRewards.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "@openzeppelin/contracts/math/SafeMath.sol"; import "./RocketDAONodeTrustedSettings.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsRewardsInterface.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; // The Trusted Node DAO Rewards settings contract RocketDAONodeTrustedSettingsRewards is RocketDAONodeTrustedSettings, RocketDAONodeTrustedSettingsRewardsInterface { using SafeMath for uint; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAONodeTrustedSettings(_rocketStorageAddress, "rewards") { // Set version version = 2; // Initialize settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings setSettingBool("rewards.network.enabled", true); // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } // Update a setting, overrides inherited setting method with extra checks for this contract function setSettingBool(string memory _settingPath, bool _value) override public onlyDAONodeTrustedProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // oDAO should never disable main net rewards if(keccak256(abi.encodePacked(_settingPath)) == keccak256(abi.encodePacked("rewards.network.enabled", uint256(0)))) { revert("Cannot disable network 0"); } } // Update setting now setBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } // Getters function getNetworkEnabled(uint256 _network) override external view returns (bool) { return getBool(keccak256(abi.encodePacked(settingNameSpace, "rewards.network.enabled", _network))); } } ================================================ FILE: contracts/contract/dao/protocol/RocketDAOProtocol.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolInterface} from "../../../interface/dao/protocol/RocketDAOProtocolInterface.sol"; import {RocketDAOProtocolProposalsInterface} from "../../../interface/dao/protocol/RocketDAOProtocolProposalsInterface.sol"; import {SettingType} from "../../../types/SettingType.sol"; import {RocketBase} from "../../RocketBase.sol"; /// @notice The Rocket Pool Protocol DAO (pDAO) contract RocketDAOProtocol is RocketBase, RocketDAOProtocolInterface { // Events event BootstrapSettingMulti(string[] settingContractNames, string[] settingPaths, SettingType[] types, bytes[] values, uint256 time); event BootstrapSettingUint(string settingContractName, string settingPath, uint256 value, uint256 time); event BootstrapSettingBool(string settingContractName, string settingPath, bool value, uint256 time); event BootstrapSettingAddress(string settingContractName, string settingPath, address value, uint256 time); event BootstrapSettingAddressList(string settingContractName, string settingPath, address[] value, uint256 time); event BootstrapSettingClaimers(uint256 trustedNodePercent, uint256 protocolPercent, uint256 nodePercent, uint256 time); event BootstrapSpendTreasury(string invoiceID, address recipientAddress, uint256 amount, uint256 time); event BootstrapTreasuryNewContract(string contractName, address recipientAddress, uint256 amountPerPeriod, uint256 periodLength, uint256 startTime, uint256 numPeriods, uint256 time); event BootstrapTreasuryUpdateContract(string contractName, address recipientAddress, uint256 amountPerPeriod, uint256 periodLength, uint256 numPeriods, uint256 time); event BootstrapSecurityInvite(string id, address memberAddress, uint256 time); event BootstrapSecurityKick(address memberAddress, uint256 time); event BootstrapDisabled(uint256 time); event BootstrapProtocolDAOEnabled(uint256 block, uint256 time); // The namespace for any data stored in the network DAO (do not change) string constant internal daoNameSpace = "dao.protocol."; // Only allow bootstrapping when enabled modifier onlyBootstrapMode() { require(getBootstrapModeDisabled() == false, "Bootstrap mode not engaged"); _; } constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 3; } /**** DAO Properties **************/ /// @notice Returns true if bootstrap mode is disabled function getBootstrapModeDisabled() override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoNameSpace, "bootstrapmode.disabled"))); } /// @notice Get the last time this user made a proposal function getMemberLastProposalTime(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.proposal.lasttime", _nodeAddress))); } /**** Bootstrapping ***************/ // While bootstrap mode is engaged, RP can change settings alongside the DAO. When disabled, only DAO will be able to control settings /// @notice Bootstrap mode - multi Setting function bootstrapSettingMulti(string[] memory _settingContractNames, string[] memory _settingPaths, SettingType[] memory _types, bytes[] memory _values) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingMulti(_settingContractNames, _settingPaths, _types, _values); emit BootstrapSettingMulti(_settingContractNames, _settingPaths, _types, _values, block.timestamp); } /// @notice Bootstrap mode - Uint Setting function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingUint(_settingContractName, _settingPath, _value); emit BootstrapSettingUint(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Bootstrap mode - Bool Setting function bootstrapSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingBool(_settingContractName, _settingPath, _value); emit BootstrapSettingBool(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Bootstrap mode - Address Setting function bootstrapSettingAddress(string memory _settingContractName, string memory _settingPath, address _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingAddress(_settingContractName, _settingPath, _value); emit BootstrapSettingAddress(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Bootstrap mode - Address list Setting function bootstrapSettingAddressList(string memory _settingContractName, string memory _settingPath, address[] calldata _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingAddressList(_settingContractName, _settingPath, _value); emit BootstrapSettingAddressList(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Bootstrap mode - Set a claiming contract to receive a % of RPL inflation rewards function bootstrapSettingClaimers(uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingRewardsClaimers(_trustedNodePercent, _protocolPercent, _nodePercent); emit BootstrapSettingClaimers(_trustedNodePercent, _protocolPercent, _nodePercent, block.timestamp); } /// @notice Bootstrap mode - Spend DAO treasury function bootstrapSpendTreasury(string memory _invoiceID, address _recipientAddress, uint256 _amount) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalTreasuryOneTimeSpend(_invoiceID, _recipientAddress, _amount); emit BootstrapSpendTreasury(_invoiceID, _recipientAddress, _amount, block.timestamp); } /// @notice Bootstrap mode - New treasury contract function bootstrapTreasuryNewContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalTreasuryNewContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _startTime, _numPeriods); emit BootstrapTreasuryNewContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _startTime, _numPeriods, block.timestamp); } /// @notice Bootstrap mode - Update treasury contract function bootstrapTreasuryUpdateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalTreasuryUpdateContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _numPeriods); emit BootstrapTreasuryUpdateContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _numPeriods, block.timestamp); } /// @notice Bootstrap mode - Invite security council member function bootstrapSecurityInvite(string memory _id, address _memberAddress) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSecurityInvite(_id, _memberAddress); emit BootstrapSecurityInvite(_id, _memberAddress, block.timestamp); } /// @notice Bootstrap mode - Kick security council member function bootstrapSecurityKick(address _memberAddress) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSecurityKick(_memberAddress); emit BootstrapSecurityKick(_memberAddress, block.timestamp); } /// @notice Bootstrap mode - Disable RP Access (only RP can call this to hand over full control to the DAO) function bootstrapDisable(bool _confirmDisableBootstrapMode) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { // Prevent disabling bootstrap if on-chain governance has not been enabled require(getUint(keccak256(abi.encodePacked("protocol.dao.enabled.block"))) > 0, "On-chain governance must be enabled first"); // Disable bootstrap require(_confirmDisableBootstrapMode == true, "You must confirm disabling bootstrap mode, it can only be done once!"); setBool(keccak256(abi.encodePacked(daoNameSpace, "bootstrapmode.disabled")), true); emit BootstrapDisabled(block.timestamp); } /// @notice Bootstrap mode - Enables on-chain governance proposals function bootstrapEnableGovernance() override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) { setUint(keccak256(abi.encodePacked("protocol.dao.enabled.block")), block.number); emit BootstrapProtocolDAOEnabled(block.number, block.timestamp); } } ================================================ FILE: contracts/contract/dao/protocol/RocketDAOProtocolActions.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../RocketBase.sol"; import "../../../interface/RocketVaultInterface.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolActionsInterface.sol"; import "../../../interface/util/IERC20Burnable.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // The Rocket Pool Network DAO Actions - This is a placeholder for the network DAO to come contract RocketDAOProtocolActions is RocketBase, RocketDAOProtocolActionsInterface { using SafeMath for uint; // The namespace for any data stored in the network DAO (do not change) string constant daoNameSpace = "dao.protocol."; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 1; } /*** Action Methods ************************/ } ================================================ FILE: contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../RocketBase.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolVerifierInterface.sol"; import "../../../interface/network/RocketNetworkVotingInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsProposalsInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityProposalsInterface.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolProposalInterface.sol"; /// @notice Manages protocol DAO proposals contract RocketDAOProtocolProposal is RocketBase, RocketDAOProtocolProposalInterface { // Events event ProposalAdded(address indexed proposer, uint256 indexed proposalID, bytes payload, uint256 time); event ProposalVoted(uint256 indexed proposalID, address indexed voter, VoteDirection direction, uint256 votingPower, uint256 time); event ProposalVoteOverridden(uint256 indexed proposalID, address indexed delegate, address indexed voter, uint256 votingPower, uint256 time); event ProposalExecuted(uint256 indexed proposalID, address indexed executor, uint256 time); event ProposalFinalised(uint256 indexed proposalID, address indexed executor, uint256 time); event ProposalDestroyed(uint256 indexed proposalID, uint256 time); // The namespace for any data stored in the protocol DAO (do not change) string constant internal daoProposalNameSpace = "dao.protocol.proposal."; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /*** Proposals **********************/ /// @notice Create a DAO proposal with calldata, if successful will be added to a queue where it can be executed /// A general message can be passed by the proposer along with the calldata payload that can be executed /// if the proposal passes /// @param _proposalMessage A string explaining what the proposal does /// @param _payload An ABI encoded payload which is executed on this contract if the proposal is successful /// @param _blockNumber The block number the proposal is being made for /// @param _treeNodes A merkle pollard generated at _blockNumber for the voting power state of the DAO function propose(string memory _proposalMessage, bytes calldata _payload, uint32 _blockNumber, Types.Node[] calldata _treeNodes) override external onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAOProtocolProposal", address(this)) returns (uint256) { // Check on-chain governance has been enabled { uint256 enabledBlock = getUint(keccak256(abi.encodePacked("protocol.dao.enabled.block"))); require(enabledBlock != 0 && _blockNumber >= enabledBlock, "DAO has not been enabled"); } // Calculate total voting power by summing the pollard uint256 totalVotingPower = 0; uint256 treeNodesLength = _treeNodes.length; for (uint256 i = 0; i < treeNodesLength; ++i) { totalVotingPower += _treeNodes[i].sum; } // Create the proposal uint256 proposalID = _propose(_proposalMessage, _blockNumber, totalVotingPower, _payload); // Add root to verifier so it can be challenged if incorrect RocketDAOProtocolVerifierInterface rocketDAOProtocolVerifier = RocketDAOProtocolVerifierInterface(getContractAddress("rocketDAOProtocolVerifier")); rocketDAOProtocolVerifier.submitProposalRoot(proposalID, msg.sender, _blockNumber, _treeNodes); return proposalID; } /// @notice Applies a vote during phase 1 /// @param _proposalID ID of the proposal to vote on /// @param _voteDirection Direction of the vote /// @param _votingPower Total delegated voting power for the voter at the proposal block /// @param _nodeIndex The index of the node voting /// @param _witness A merkle proof into the network voting power tree proving the supplied voting power is correct function vote(uint256 _proposalID, VoteDirection _voteDirection, uint256 _votingPower, uint256 _nodeIndex, Types.Node[] calldata _witness) external onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAOProtocolProposal", address(this)) { // Check valid vote require(_voteDirection != VoteDirection.NoVote, "Invalid vote"); // Check the proposal is in a state that can be voted on require(getState(_proposalID) == ProposalState.ActivePhase1, "Phase 1 voting is not active"); // Verify the voting power is correct RocketDAOProtocolVerifierInterface rocketDAOProtocolVerifier = RocketDAOProtocolVerifierInterface(getContractAddress("rocketDAOProtocolVerifier")); require(rocketDAOProtocolVerifier.verifyVote(msg.sender, _nodeIndex, _proposalID, _votingPower, _witness), "Invalid proof"); // Apply vote _vote(msg.sender, _votingPower, _proposalID, _voteDirection, true); } /// @notice Applies a vote during phase 2 (can be used to override vote direction of delegate) /// @param _proposalID ID of the proposal to vote on /// @param _voteDirection Direction of the vote function overrideVote(uint256 _proposalID, VoteDirection _voteDirection) override external onlyRegisteredNode(msg.sender) onlyLatestContract("rocketDAOProtocolProposal", address(this)) { // Check valid vote require(_voteDirection != VoteDirection.NoVote, "Invalid vote"); // Check the proposal is in a state that can be voted on require(getState(_proposalID) == ProposalState.ActivePhase2, "Phase 2 voting is not active"); // Load contracts RocketNetworkVotingInterface rocketNetworkVoting = RocketNetworkVotingInterface(getContractAddress("rocketNetworkVoting")); // Get caller's voting power and direction of their delegate uint32 blockNumber = uint32(getProposalBlock(_proposalID)); uint256 votingPower = rocketNetworkVoting.getVotingPower(msg.sender, blockNumber); address delegate = rocketNetworkVoting.getDelegate(msg.sender, blockNumber); // Check if delegate voted in phase 1 if (getReceiptHasVotedPhase1(_proposalID, delegate)) { // Get the vote direction of their delegate VoteDirection delegateVote = getReceiptDirection(_proposalID, delegate); require (delegateVote != _voteDirection, "Vote direction is the same as delegate"); // Reverse the delegate's vote _overrideVote(delegate, msg.sender, _proposalID, votingPower, delegateVote); } // Apply this voter's vote _vote(msg.sender, votingPower, _proposalID, _voteDirection, false); } /// @notice Finalises a vetoed proposal by burning the proposer's bond /// @param _proposalID ID of the proposal to finalise function finalise(uint256 _proposalID) override external onlyLatestContract("rocketDAOProtocolProposal", address(this)) { // Check state require(getState(_proposalID) == ProposalState.Vetoed, "Proposal has not been vetoed"); bytes32 finalisedKey = keccak256(abi.encodePacked(daoProposalNameSpace, "finalised", _proposalID)); require(getBool(finalisedKey) == false, "Proposal already finalised"); setBool(finalisedKey, true); // Burn the proposer's bond RocketDAOProtocolVerifierInterface rocketDAOProtocolVerifier = RocketDAOProtocolVerifierInterface(getContractAddress("rocketDAOProtocolVerifier")); rocketDAOProtocolVerifier.burnProposalBond(_proposalID); // Log it emit ProposalFinalised(_proposalID, tx.origin, block.timestamp); } /// @notice Executes a successful proposal /// @param _proposalID ID of the proposal to execute function execute(uint256 _proposalID) override external onlyLatestContract("rocketDAOProtocolProposal", address(this)) { // Firstly make sure this proposal has passed require(getState(_proposalID) == ProposalState.Succeeded, "Proposal has not succeeded, has expired or has already been executed"); // Set as executed now before running payload setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "executed", _proposalID)), true); // Get the proposals contract address daoProtocolProposalsAddress = getContractAddress("rocketDAOProtocolProposals"); // Ok all good, lets run the payload on the dao contract that the proposal relates too, it should execute one of the methods on this contract (bool success, bytes memory response) = daoProtocolProposalsAddress.call(getPayload(_proposalID)); // Was there an error? require(success, getRevertMsg(response)); // Log it emit ProposalExecuted(_proposalID, tx.origin, block.timestamp); } /// @dev Called by the verifier contract to destroy a proven invalid proposal function destroy(uint256 _proposalID) override external onlyLatestContract("rocketDAOProtocolProposal", address(this)) onlyLatestContract("rocketDAOProtocolVerifier", msg.sender) { // Cancel the proposal bytes32 destroyedKey = keccak256(abi.encodePacked(daoProposalNameSpace, "destroyed", _proposalID)); require(getBool(destroyedKey) == false, "Proposal already destroyed"); setBool(destroyedKey, true); // Log it emit ProposalDestroyed(_proposalID, block.timestamp); } /// @notice Gets the block used to generate a proposal /// @param _proposalID The ID of the proposal to query /// @return The block used to generated the requested proposal function getProposalBlock(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "proposal.block", _proposalID))); } /// @notice Gets the amount of vetos required to stop a proposal /// @param _proposalID The ID of the proposal to veto /// @return The amount of voting power required to veto a proposal function getProposalVetoQuorum(uint256 _proposalID) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "proposal.veto.quorum", _proposalID))); } /// @notice Get the current total proposals function getTotal() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "total"))); } /// @notice Get the member who proposed /// @param _proposalID The ID of the proposal to query function getProposer(uint256 _proposalID) override public view returns (address) { return getAddress(keccak256(abi.encodePacked(daoProposalNameSpace, "proposer", _proposalID))); } /// @notice Get the proposal message /// @param _proposalID The ID of the proposal to query function getMessage(uint256 _proposalID) override external view returns (string memory) { return getString(keccak256(abi.encodePacked(daoProposalNameSpace, "message", _proposalID))); } /// @notice Get the start of this proposal as a timestamp /// @param _proposalID The ID of the proposal to query function getStart(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "start", _proposalID))); } /// @notice Get the end of phase1 of this proposal as a timestamp /// @param _proposalID The ID of the proposal to query function getPhase1End(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "phase1End", _proposalID))); } /// @notice Get the end of phase2 of this proposal as a timestamp /// @param _proposalID The ID of the proposal to query /// @return timestamp for the end of phase2 function getPhase2End(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "phase2End", _proposalID))); } /// @notice The timestamp where the proposal expires and can no longer be executed if it is successful /// @param _proposalID The ID of the proposal to query function getExpires(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "expires", _proposalID))); } /// @notice Get the created status of this proposal /// @param _proposalID The ID of the proposal to query function getCreated(uint256 _proposalID) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "created", _proposalID))); } /// @notice Get the for voting power count of this proposal /// @param _proposalID The ID of the proposal to query function getVotingPowerFor(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", _proposalID))); } /// @notice Get the against voting power count of this proposal /// @param _proposalID The ID of the proposal to query function getVotingPowerAgainst(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", _proposalID))); } /// @notice Get the veto voting power count of this proposal /// @param _proposalID The ID of the proposal to query function getVotingPowerVeto(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.veto", _proposalID))); } /// @notice Get the against voteing power count of this proposal /// @param _proposalID The ID of the proposal to query function getVotingPowerAbstained(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.abstained", _proposalID))); } /// @notice How much voting power is required for the proposal to succeed /// @param _proposalID The ID of the proposal to query function getVotingPowerRequired(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.required", _proposalID))); } /// @notice Get the destroyed status of this proposal /// @param _proposalID The ID of the proposal to query function getDestroyed(uint256 _proposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "destroyed", _proposalID))); } /// @notice Get the finalised status of this proposal /// @param _proposalID The ID of the proposal to query function getFinalised(uint256 _proposalID) override external view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "finalised", _proposalID))); } /// @notice Get the executed status of this proposal /// @param _proposalID The ID of the proposal to query function getExecuted(uint256 _proposalID) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "executed", _proposalID))); } /// @notice Get the amount of veto votes required to veto this proposal /// @param _proposalID The ID of the proposal to query function getVetoQuorum(uint256 _proposalID) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "proposal.veto.quorum", _proposalID))); } /// @notice Get the veto status of this proposal /// @param _proposalID The ID of the proposal to query function getVetoed(uint256 _proposalID) override public view returns (bool) { uint256 votesVeto = getVotingPowerVeto(_proposalID); uint256 quorum = getVetoQuorum(_proposalID); return votesVeto >= quorum; } /// @notice Get the proposal payload /// @param _proposalID The ID of the proposal to query function getPayload(uint256 _proposalID) override public view returns (bytes memory) { return getBytes(keccak256(abi.encodePacked(daoProposalNameSpace, "payload", _proposalID))); } /// @notice Returns true if this proposal has already been voted on by a node /// @param _proposalID The ID of the proposal to query /// @param _nodeAddress The node operator address to query function getReceiptHasVoted(uint256 _proposalID, address _nodeAddress) override public view returns (bool) { return getReceiptDirection(_proposalID, _nodeAddress) != VoteDirection.NoVote; } /// @notice Returns true if this proposal has been voted on in phase 1 by a node /// @param _proposalID The ID of the proposal to query /// @param _nodeAddress The node operator address to query function getReceiptHasVotedPhase1(uint256 _proposalID, address _nodeAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.phase1", _proposalID, _nodeAddress))); } /// @notice Returns the direction a node voted on a given proposal /// @param _proposalID The ID of the proposal to query /// @param _nodeAddress The node operator address to query function getReceiptDirection(uint256 _proposalID, address _nodeAddress) override public view returns (VoteDirection) { return VoteDirection(getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.direction", _proposalID, _nodeAddress)))); } /// @notice Return the state of the specified proposal /// @param _proposalID The ID of the proposal to query function getState(uint256 _proposalID) override public view returns (ProposalState) { // Check the proposal ID is legit require(getTotal() >= _proposalID && _proposalID > 0, "Invalid proposal ID"); // Destroyed? if (getDestroyed(_proposalID)) { return ProposalState.Destroyed; } // Has it been executed? else if (getExecuted(_proposalID)) { return ProposalState.Executed; } else { uint256 start = getStart(_proposalID); // Is the proposal pending? if (block.timestamp < start) { return ProposalState.Pending; } else { // The proposal is active and can be voted on uint256 phase1End = getPhase1End(_proposalID); uint256 phase2End = getPhase2End(_proposalID); if (block.timestamp < phase1End) { return ProposalState.ActivePhase1; } else if (block.timestamp < phase2End) { return ProposalState.ActivePhase2; } else { // Is the proposal vetoed? if (getVetoed(_proposalID)) { return ProposalState.Vetoed; } uint256 votesFor = getVotingPowerFor(_proposalID); uint256 votesAgainst = getVotingPowerAgainst(_proposalID); uint256 votesAbstained = getVotingPowerAbstained(_proposalID); uint256 totalVotes = votesFor + votesAgainst + votesAbstained; // Has the proposal reached quorum? if (totalVotes >= getVotingPowerRequired(_proposalID)) { if (votesFor > votesAgainst) { if (block.timestamp < getExpires(_proposalID)) { // Vote was successful, is now awaiting execution return ProposalState.Succeeded; } } else { // Vote was defeated return ProposalState.Defeated; } } else { return ProposalState.QuorumNotMet; } } } } return ProposalState.Expired; } /// @dev Internal function to generate a proposal /// @param _proposalMessage the message associated with the proposal /// @param _blockNumber the block number considered for the proposal snapshot /// @param _totalVotingPower the total voting power for the proposal - used to calculate quorum /// @param _payload A calldata payload to execute after the proposal is successful /// @return The new proposal's ID function _propose(string memory _proposalMessage, uint256 _blockNumber, uint256 _totalVotingPower, bytes calldata _payload) internal returns (uint256) { // Validate block number require(_blockNumber < block.number, "Block must be in the past"); // Load contracts RocketDAOProtocolSettingsProposalsInterface rocketDAOProtocolSettingsProposals = RocketDAOProtocolSettingsProposalsInterface(getContractAddress("rocketDAOProtocolSettingsProposals")); require(_blockNumber + rocketDAOProtocolSettingsProposals.getProposalMaxBlockAge() > block.number, "Block too old"); // Calculate quorums uint256 quorum = 0; uint256 vetoQuorum = 0; { uint256 proposalQuorum = rocketDAOProtocolSettingsProposals.getProposalQuorum(); uint256 vetoProposalQuorum = rocketDAOProtocolSettingsProposals.getProposalVetoQuorum(); quorum = _totalVotingPower * proposalQuorum / calcBase; vetoQuorum = _totalVotingPower * vetoProposalQuorum / calcBase; } // Add proposal return _addProposal( msg.sender, _proposalMessage, _blockNumber, block.timestamp + rocketDAOProtocolSettingsProposals.getVoteDelayTime(), rocketDAOProtocolSettingsProposals.getVotePhase1Time(), rocketDAOProtocolSettingsProposals.getVotePhase2Time(), rocketDAOProtocolSettingsProposals.getExecuteTime(), quorum, vetoQuorum, _payload ); } /// @dev Add a proposal to the protocol DAO function _addProposal(address _proposer, string memory _message, uint256 _blockNumber, uint256 _startTime, uint256 _phase1Duration, uint256 _phase2Duration, uint256 _expires, uint256 _votesRequired, uint256 _vetoQuorum, bytes calldata _payload) internal returns (uint256) { // Basic checks require(_startTime > block.timestamp, "Proposal start time must be in the future"); require(_phase1Duration > 0, "Proposal cannot have a duration of 0"); require(_phase2Duration > 0, "Proposal cannot have a duration of 0"); require(_expires > 0, "Proposal cannot have a execution expiration of 0"); require(_votesRequired > 0, "Proposal cannot have a 0 votes required to be successful"); // Set the expires block uint256 expires = _startTime + _phase1Duration + _phase2Duration + _expires; // Get the proposal ID uint256 proposalID = getTotal() + 1; // The data structure for a proposal setAddress(keccak256(abi.encodePacked(daoProposalNameSpace, "proposer", proposalID)), _proposer); // Which node is making the proposal setString(keccak256(abi.encodePacked(daoProposalNameSpace, "message", proposalID)), _message); // A general message that can be included with the proposal setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "start", proposalID)), _startTime); // The time the proposal becomes active for voting on setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "phase1End", proposalID)), _startTime + _phase1Duration); // The time the proposal where voting ends on phase 1 setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "phase2End", proposalID)), _startTime + _phase1Duration + _phase2Duration); // The time the proposal where voting ends on phase 2 setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "expires", proposalID)), expires); // The time when the proposal expires and can no longer be executed if it is successful setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "created", proposalID)), block.timestamp); // The time the proposal was created at setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.required", proposalID)), _votesRequired); // How many votes are required for the proposal to pass setBytes(keccak256(abi.encodePacked(daoProposalNameSpace, "payload", proposalID)), _payload); // A calldata payload to execute after it is successful setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "proposal.block", proposalID)), uint256(_blockNumber)); // The block that the network voting power tree was generated for for this proposal setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "proposal.veto.quorum", proposalID)), _vetoQuorum); // The number of veto votes required to veto this proposal // Update the total proposals setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "total")), proposalID); // Log it emit ProposalAdded(_proposer, proposalID, _payload, block.timestamp); // Done return proposalID; } /// @dev Internal method to override the vote of a delegate function _overrideVote(address _delegate, address _voter, uint256 _proposalID, uint256 _votes, VoteDirection _voteDirection) internal { // Check for non-zero voting power require(_votes > 0, "Cannot vote with 0 voting power"); // Remove votes from proposal if (_voteDirection == VoteDirection.For) { subUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", _proposalID)), _votes); } else if(_voteDirection == VoteDirection.Abstain) { subUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.abstained", _proposalID)), _votes); } else { if(_voteDirection == VoteDirection.AgainstWithVeto) { subUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.veto", _proposalID)), _votes); } subUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", _proposalID)), _votes); } // Reduce the voting power applied by the delegate to this proposal subUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.votes", _proposalID, _delegate)), _votes); // Log it emit ProposalVoteOverridden(_proposalID, _delegate, _voter, _votes, block.timestamp); } /// @dev Internal method to apply voting power against a proposal function _vote(address _nodeOperator, uint256 _votes, uint256 _proposalID, VoteDirection _voteDirection, bool _phase1) internal { // Check for non-zero voting power require(_votes > 0, "Cannot vote with 0 voting power"); // Has this node already voted on this proposal? require(!getReceiptHasVoted(_proposalID, _nodeOperator), "Node operator has already voted on proposal"); // Add votes to proposal if (_voteDirection == VoteDirection.For) { addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.for", _proposalID)), _votes); } else if(_voteDirection == VoteDirection.Abstain) { addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.abstained", _proposalID)), _votes); } else { if(_voteDirection == VoteDirection.AgainstWithVeto) { addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.veto", _proposalID)), _votes); } addUint(keccak256(abi.encodePacked(daoProposalNameSpace, "votes.against", _proposalID)), _votes); } // Record the vote receipt now setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.votes", _proposalID, _nodeOperator)), _votes); setUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.direction", _proposalID, _nodeOperator)), uint256(_voteDirection)); // Record delegate voted in phase 1 if (_phase1) { setBool(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.phase1", _proposalID, _nodeOperator)), true); } // Log it emit ProposalVoted(_proposalID, _nodeOperator, _voteDirection, _votes, block.timestamp); } } ================================================ FILE: contracts/contract/dao/protocol/RocketDAOProtocolProposals.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../RocketBase.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolInterface.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolProposalsInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import "../../../interface/rewards/claims/RocketClaimDAOInterface.sol"; import "../../../interface/dao/RocketDAOProposalInterface.sol"; import "../../../interface/node/RocketNodeManagerInterface.sol"; import "../../../types/SettingType.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolVerifierInterface.sol"; import "../../../interface/network/RocketNetworkVotingInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsProposalsInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityProposalsInterface.sol"; /// @notice Manages protocol DAO proposals contract RocketDAOProtocolProposals is RocketBase, RocketDAOProtocolProposalsInterface { // Events event ProposalSettingUint(string settingContractName, string settingPath, uint256 value, uint256 time); event ProposalSettingBool(string settingContractName, string settingPath, bool value, uint256 time); event ProposalSettingAddress(string settingContractName, string settingPath, address value, uint256 time); event ProposalSettingAddressList(string settingContractName, string settingPath, address[] value, uint256 time); event ProposalSettingRewardsClaimers(uint256 trustedNodePercent, uint256 protocolPercent, uint256 nodePercent, uint256 time); event ProposalSecurityInvite(string id, address memberAddress, uint256 time); event ProposalSecurityKick(address memberAddress, uint256 time); event ProposalSecurityKickMulti(address[] memberAddresses, uint256 time); event ProposalSecurityReplace(address existingMemberAddress, string newMemberId, address newMemberAddress, uint256 time); // Only allow certain contracts to execute methods modifier onlyExecutingContracts() { // Methods are either executed by bootstrapping methods in rocketDAONodeTrusted or by people executing passed proposals on this contract require(msg.sender == getContractAddress("rocketDAOProtocol") || msg.sender == getContractAddress("rocketDAOProtocolProposal"), "Sender is not permitted to access executing methods"); _; } constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 3; } /*** Proposal - Settings ***************/ /// @notice Set multiple settings in one proposal. It is required that all input arrays are the same length. /// @param _settingContractNames An array of contract names /// @param _settingPaths An array of setting paths /// @param _types An array of the type of values to set /// @param _data An array of ABI encoded values to set function proposalSettingMulti(string[] memory _settingContractNames, string[] memory _settingPaths, SettingType[] memory _types, bytes[] memory _data) override external onlyExecutingContracts() { // Check lengths of all arguments are the same require(_settingContractNames.length == _settingPaths.length && _settingPaths.length == _types.length && _types.length == _data.length, "Invalid parameters supplied"); // Loop through settings for (uint256 i = 0; i < _settingContractNames.length; ++i) { if (_types[i] == SettingType.UINT256) { uint256 value = abi.decode(_data[i], (uint256)); proposalSettingUint(_settingContractNames[i], _settingPaths[i], value); } else if (_types[i] == SettingType.BOOL) { bool value = abi.decode(_data[i], (bool)); proposalSettingBool(_settingContractNames[i], _settingPaths[i], value); } else if (_types[i] == SettingType.ADDRESS) { address value = abi.decode(_data[i], (address)); proposalSettingAddress(_settingContractNames[i], _settingPaths[i], value); } else { revert("Invalid setting type"); } } } /// @notice Change one of the current uint256 settings of the protocol DAO /// @param _settingContractName Contract name of the setting to change /// @param _settingPath Setting path to change /// @param _value New setting value function proposalSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override public onlyExecutingContracts() { RocketDAOProtocolSettingsInterface rocketDAOProtocolSettings = RocketDAOProtocolSettingsInterface(getContractAddress(_settingContractName)); rocketDAOProtocolSettings.setSettingUint(_settingPath, _value); emit ProposalSettingUint(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Change one of the current bool settings of the protocol DAO /// @param _settingContractName Contract name of the setting to change /// @param _settingPath Setting path to change /// @param _value New setting value function proposalSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) override public onlyExecutingContracts() { RocketDAOProtocolSettingsInterface rocketDAOProtocolSettings = RocketDAOProtocolSettingsInterface(getContractAddress(_settingContractName)); rocketDAOProtocolSettings.setSettingBool(_settingPath, _value); emit ProposalSettingBool(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Change one of the current address settings of the protocol DAO /// @param _settingContractName Contract name of the setting to change /// @param _settingPath Setting path to change /// @param _value New setting value function proposalSettingAddress(string memory _settingContractName, string memory _settingPath, address _value) override public onlyExecutingContracts() { RocketDAOProtocolSettingsInterface rocketDAOProtocolSettings = RocketDAOProtocolSettingsInterface(getContractAddress(_settingContractName)); rocketDAOProtocolSettings.setSettingAddress(_settingPath, _value); emit ProposalSettingAddress(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Change one of the current address[] settings of the protocol DAO /// @param _settingContractName Contract name of the setting to change /// @param _settingPath Setting path to change /// @param _value[] New setting value function proposalSettingAddressList(string memory _settingContractName, string memory _settingPath, address[] calldata _value) override public onlyExecutingContracts() { RocketDAOProtocolSettingsInterface rocketDAOProtocolSettings = RocketDAOProtocolSettingsInterface(getContractAddress(_settingContractName)); rocketDAOProtocolSettings.setSettingAddressList(_settingPath, _value); emit ProposalSettingAddressList(_settingContractName, _settingPath, _value, block.timestamp); } /// @notice Updates the percentages the trusted nodes use when calculating RPL reward trees. Percentages must add up to 100% /// @param _trustedNodePercent The percentage of rewards paid to the trusted node set (as a fraction of 1e18) /// @param _protocolPercent The percentage of rewards paid to the protocol dao (as a fraction of 1e18) /// @param _nodePercent The percentage of rewards paid to the node operators (as a fraction of 1e18) function proposalSettingRewardsClaimers(uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent) override external onlyExecutingContracts() { RocketDAOProtocolSettingsRewardsInterface rocketDAOProtocolSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); rocketDAOProtocolSettingsRewards.setSettingRewardsClaimers(_trustedNodePercent, _protocolPercent, _nodePercent); emit ProposalSettingRewardsClaimers(_trustedNodePercent, _protocolPercent, _nodePercent, block.timestamp); } /// @notice Spend RPL from the DAO's treasury immediately /// @param _invoiceID Arbitrary string for book keeping /// @param _recipientAddress Address to receive the RPL /// @param _amount Amount of RPL to send function proposalTreasuryOneTimeSpend(string memory _invoiceID, address _recipientAddress, uint256 _amount) override external onlyExecutingContracts() { RocketClaimDAOInterface rocketDAOTreasury = RocketClaimDAOInterface(getContractAddress("rocketClaimDAO")); rocketDAOTreasury.spend(_invoiceID, _recipientAddress, _amount); } /// @notice Add a new recurring payment contract to the treasury /// @param _contractName A unique string to refer to this payment contract /// @param _recipientAddress Address to receive the periodic RPL /// @param _amountPerPeriod Amount of RPL to pay per period /// @param _periodLength Number of seconds between each period /// @param _startTime Timestamp of when payments should begin /// @param _numPeriods Number periods to pay, or zero for a never ending contract function proposalTreasuryNewContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) override external onlyExecutingContracts() { RocketClaimDAOInterface rocketDAOTreasury = RocketClaimDAOInterface(getContractAddress("rocketClaimDAO")); rocketDAOTreasury.newContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _startTime, _numPeriods); } /// @notice Modifies and existing recurring payment contract /// @param _contractName The unique string of the payment contract /// @param _recipientAddress New address to receive the periodic RPL /// @param _amountPerPeriod New amount of RPL to pay per period /// @param _periodLength New number of seconds between each period /// @param _numPeriods New number periods to pay, or zero for a never ending contract function proposalTreasuryUpdateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) override external onlyExecutingContracts() { RocketClaimDAOInterface rocketDAOTreasury = RocketClaimDAOInterface(getContractAddress("rocketClaimDAO")); rocketDAOTreasury.updateContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _numPeriods); } /// @notice Invites an address to join the security council /// @param _id A string to identify this member with /// @param _memberAddress The address of the new member function proposalSecurityInvite(string calldata _id, address _memberAddress) override external onlyExecutingContracts() { RocketDAOSecurityProposalsInterface rocketDAOSecurityProposals = RocketDAOSecurityProposalsInterface(getContractAddress("rocketDAOSecurityProposals")); rocketDAOSecurityProposals.proposalInvite(_id, _memberAddress); emit ProposalSecurityInvite(_id, _memberAddress, block.timestamp); } /// @notice Propose to kick a current member from the security council /// @param _memberAddress The address of the member to kick function proposalSecurityKick(address _memberAddress) override external onlyExecutingContracts() { RocketDAOSecurityProposalsInterface rocketDAOSecurityProposals = RocketDAOSecurityProposalsInterface(getContractAddress("rocketDAOSecurityProposals")); rocketDAOSecurityProposals.proposalKick(_memberAddress); emit ProposalSecurityKick(_memberAddress, block.timestamp); } /// @notice Propose to kick multiple current members from the security council /// @param _memberAddresses An array of addresses of the members to kick function proposalSecurityKickMulti(address[] calldata _memberAddresses) override external onlyExecutingContracts() { RocketDAOSecurityProposalsInterface rocketDAOSecurityProposals = RocketDAOSecurityProposalsInterface(getContractAddress("rocketDAOSecurityProposals")); rocketDAOSecurityProposals.proposalKickMulti(_memberAddresses); emit ProposalSecurityKickMulti(_memberAddresses, block.timestamp); } /// @notice Propose to replace a current member from the security council /// @param _existingMemberAddress The address of the member to kick /// @param _newMemberId A string to identify this member with /// @param _newMemberAddress The address of the new member function proposalSecurityReplace(address _existingMemberAddress, string calldata _newMemberId, address _newMemberAddress) override external onlyExecutingContracts() { RocketDAOSecurityProposalsInterface rocketDAOSecurityProposals = RocketDAOSecurityProposalsInterface(getContractAddress("rocketDAOSecurityProposals")); rocketDAOSecurityProposals.proposalReplace(_existingMemberAddress, _newMemberId, _newMemberAddress); emit ProposalSecurityReplace(_existingMemberAddress, _newMemberId, _newMemberAddress, block.timestamp); } } ================================================ FILE: contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../RocketBase.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolVerifierInterface.sol"; import "../../../interface/network/RocketNetworkVotingInterface.sol"; import "../../../interface/node/RocketNodeManagerInterface.sol"; import "@openzeppelin4/contracts/utils/math/Math.sol"; import "../../../interface/token/RocketTokenRPLInterface.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolProposalsInterface.sol"; import "../../../interface/dao/RocketDAOProposalInterface.sol"; import "../../../interface/node/RocketNodeStakingInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsProposalsInterface.sol"; import "../../../interface/dao/protocol/RocketDAOProtocolProposalInterface.sol"; /// @notice Implements the protocol DAO optimistic fraud proof proposal system contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInterface { uint256 constant internal depthPerRound = 5; // Packing constants for packing challenge data into a single uint256 uint256 constant internal stateOffset = (256 - 8); uint256 constant internal timestampOffset = (256 - 8 - 64); uint256 constant internal addressOffset = (256 - 8 - 64 - 160); // Offsets into storage for proposal details uint256 constant internal proposerOffset = 0; uint256 constant internal blockNumberOffset = 1; uint256 constant internal nodeCountOffset = 2; uint256 constant internal defeatIndexOffset = 3; uint256 constant internal proposalBondOffset = 4; uint256 constant internal challengeBondOffset = 5; uint256 constant internal challengePeriodOffset = 6; // Offsets into storage for challenge details uint256 constant internal challengeStateOffset = 0; uint256 constant internal sumOffset = 1; uint256 constant internal hashOffset = 2; // Burn rate uint256 constant internal bondBurnPercent = 0.2 ether; // Events event RootSubmitted(uint256 indexed proposalID, address indexed proposer, uint32 blockNumber, uint256 index, Types.Node root, Types.Node[] treeNodes, uint256 timestamp); event ChallengeSubmitted(uint256 indexed proposalID, address indexed challenger, uint256 index, uint256 timestamp); event ProposalBondBurned(uint256 indexed proposalID, address indexed proposer, uint256 amount, uint256 timestamp); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 2; } /// @notice Returns the depth per round function getDepthPerRound() override external pure returns (uint256) { return depthPerRound; } /// @notice Returns the defeat index for this proposal /// @param _proposalID The proposal to fetch details function getDefeatIndex(uint256 _proposalID) override external view returns (uint256) { // Fetch the proposal key uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); return getUint(bytes32(proposalKey + defeatIndexOffset)); } /// @notice Returns the proposal bond for this proposal /// @param _proposalID The proposal to fetch details function getProposalBond(uint256 _proposalID) override external view returns (uint256) { // Fetch the proposal key uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); return getUint(bytes32(proposalKey + proposalBondOffset)); } /// @notice Returns the challenge bond for this proposal /// @param _proposalID The proposal to fetch details function getChallengeBond(uint256 _proposalID) override external view returns (uint256) { // Fetch the proposal key uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); return getUint(bytes32(proposalKey + challengeBondOffset)); } /// @notice Returns the duration of the challenge period for this proposal /// @param _proposalID The proposal to fetch details function getChallengePeriod(uint256 _proposalID) override external view returns (uint256) { // Fetch the proposal key uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); return getUint(bytes32(proposalKey + challengePeriodOffset)); } /// @dev Called during a proposal submission to calculate and store the proposal root so it is available for challenging /// @param _proposalID The ID of the proposal /// @param _proposer The node raising the proposal /// @param _blockNumber The block number used to generate the voting power tree /// @param _treeNodes A pollard of the voting power tree function submitProposalRoot(uint256 _proposalID, address _proposer, uint32 _blockNumber, Types.Node[] calldata _treeNodes) external onlyLatestContract("rocketDAOProtocolProposal", msg.sender) onlyLatestContract("rocketDAOProtocolVerifier", address(this)) { // Retrieve the node count at _blockNumber uint256 nodeCount; { RocketNetworkVotingInterface rocketNetworkVoting = RocketNetworkVotingInterface(getContractAddress("rocketNetworkVoting")); nodeCount = rocketNetworkVoting.getNodeCount(_blockNumber); } // Verify proposer supplied correct number of nodes for the pollard { uint256 maxDepth = getMaxDepth(nodeCount); if (maxDepth < depthPerRound) { uint256 leafCount = 2 ** maxDepth; require(_treeNodes.length == leafCount, "Invalid node count"); } else { require(_treeNodes.length == 2 ** depthPerRound, "Invalid node count"); } } // Compute the proposal root from the supplied nodes Types.Node memory root = computeRootFromNodes(_treeNodes); { RocketDAOProtocolSettingsProposalsInterface rocketDAOProtocolSettingsProposals = RocketDAOProtocolSettingsProposalsInterface(getContractAddress("rocketDAOProtocolSettingsProposals")); // Get the current proposal bond amount uint256 proposalBond = rocketDAOProtocolSettingsProposals.getProposalBond(); // Lock the proposal bond (will revert if proposer doesn't have enough effective RPL staked) RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStaking.lockRPL(_proposer, proposalBond); // Store proposal details uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); setAddress(bytes32(proposalKey + proposerOffset), _proposer); setUint(bytes32(proposalKey + blockNumberOffset), _blockNumber); setUint(bytes32(proposalKey + nodeCountOffset), nodeCount); setUint(bytes32(proposalKey + proposalBondOffset), proposalBond); setUint(bytes32(proposalKey + challengeBondOffset), rocketDAOProtocolSettingsProposals.getChallengeBond()); setUint(bytes32(proposalKey + challengePeriodOffset), rocketDAOProtocolSettingsProposals.getChallengePeriod()); } // The root was supplied so mark that index (1) as responded and store the node setNode(_proposalID, 1, root); uint256 state = uint256(Types.ChallengeState.Responded) << stateOffset; state |= block.timestamp << timestampOffset; setUint(keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, uint256(1))), state); // Emit event emit RootSubmitted(_proposalID, _proposer, _blockNumber, 1, root, _treeNodes, block.timestamp); } /// @dev Called by proposal contract to burn the bond of the proposer after a successful veto /// @param _proposalID the proposal ID that will have the bond burnt function burnProposalBond(uint256 _proposalID) override external onlyLatestContract("rocketDAOProtocolProposal", address(msg.sender)) onlyLatestContract("rocketDAOProtocolVerifier", address(this)) { // Retrieved required inputs from storage uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); address proposer = getAddress(bytes32(proposalKey + proposerOffset)); uint256 proposalBond = getUint(bytes32(proposalKey + proposalBondOffset)); // Unlock and burn RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStaking.unlockRPL(proposer, proposalBond); rocketNodeStaking.burnRPL(proposer, proposalBond); // Log it emit ProposalBondBurned(_proposalID, proposer, proposalBond, block.timestamp); } /// @notice Used by a verifier to challenge a specific index of a proposal's voting power tree /// @param _proposalID The ID of the proposal being challenged /// @param _index The global index of the node being challenged /// @param _node The node that is being challenged as submitted by the proposer /// @param _witness A merkle proof of the challenged node (using the previously challenged index as a root) function createChallenge(uint256 _proposalID, uint256 _index, Types.Node calldata _node, Types.Node[] calldata _witness) external onlyLatestContract("rocketDAOProtocolVerifier", address(this)) onlyRegisteredNode(msg.sender) { { // Scope to prevent stack too deep // Check whether the proposal is on the Pending state RocketDAOProtocolProposalInterface daoProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); RocketDAOProtocolProposalInterface.ProposalState proposalState = daoProposal.getState(_proposalID); require(proposalState == RocketDAOProtocolProposalInterface.ProposalState.Pending, "Can only challenge while proposal is Pending"); } // Precompute the proposal key uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); // Retrieve the node count of this proposal uint256 nodeCount = getUint(bytes32(proposalKey + nodeCountOffset)); uint256 maxDepth = getMaxDepth(nodeCount); { // Check depth doesn't exceed the extended tree uint256 depth = getDepthFromIndex(_index); require(depth < maxDepth * 2, "Invalid index depth"); } // Check for existing challenge against this index { bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index)); uint256 challengeData = getUint(challengeKey); require(challengeData == 0, "Index already challenged"); // Write challenge challengeData = uint256(Types.ChallengeState.Challenged) << stateOffset; challengeData |= block.timestamp << timestampOffset; challengeData |= uint256(uint160(msg.sender)) << addressOffset; setUint(challengeKey, challengeData); } // Check the proposal hasn't already been defeated require(getUint(bytes32(proposalKey+defeatIndexOffset)) == 0, "Proposal already defeated"); // Verify the validity of the challenge proof { // Check depth is exactly one round deeper than a previous challenge (or the proposal root) uint256 previousIndex = getPollardRootIndex(_index, nodeCount); require(_getChallengeState(getUint(keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, previousIndex)))) == Types.ChallengeState.Responded, "Invalid challenge depth"); // Check the proof contains the expected number of nodes require(_witness.length == getDepthFromIndex(_index) - getDepthFromIndex(previousIndex), "Invalid proof length"); // Get expected node and compute provided root node then compare Types.Node memory _expected = getNode(_proposalID, previousIndex); Types.Node memory rootFromWitness = computeRootFromWitness(_index, _node, _witness); require(rootFromWitness.hash == _expected.hash, "Invalid hash"); require(rootFromWitness.sum == _expected.sum, "Invalid sum"); // Store the node setNode(_proposalID, _index, _node); } // Lock the challenger's bond (reverts if not enough effective RPL) { uint256 challengeBond = getUint(bytes32(proposalKey + challengeBondOffset)); RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStaking.lockRPL(msg.sender, challengeBond); } // Emit event emit ChallengeSubmitted(_proposalID, msg.sender, _index, block.timestamp); } /// @notice Can be called if proposer fails to respond to a challenge within the required time limit. Destroys the proposal if successful /// @param _proposalID The ID of the challenged proposal /// @param _index The index which was failed to respond to function defeatProposal(uint256 _proposalID, uint256 _index) external onlyLatestContract("rocketDAOProtocolVerifier", address(this)) onlyRegisteredNode(msg.sender) { { // Scope to prevent stack too deep // Check whether the proposal is in the Pending state RocketDAOProtocolProposalInterface daoProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); RocketDAOProtocolProposalInterface.ProposalState proposalState = daoProposal.getState(_proposalID); require(proposalState == RocketDAOProtocolProposalInterface.ProposalState.Pending, "Can not defeat a valid proposal"); } // Check the challenge at the given index has not been responded to bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index)); uint256 data = getUint(challengeKey); Types.ChallengeState state = _getChallengeState(data); require(state == Types.ChallengeState.Challenged, "Invalid challenge state"); uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); // Precompute defeat index key bytes32 defeatIndexKey = bytes32(proposalKey+defeatIndexOffset); uint256 challengePeriod = getUint(bytes32(proposalKey + challengePeriodOffset)); // Check the proposal hasn't already been defeated uint256 defeatIndex = getUint(defeatIndexKey); require(defeatIndex == 0, "Proposal already defeated"); // Check enough time has passed uint256 timestamp = getChallengeTimestamp(data); require(block.timestamp > timestamp + challengePeriod, "Not enough time has passed"); // Destroy the proposal RocketDAOProtocolProposalInterface rocketDAOProtocolProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); rocketDAOProtocolProposal.destroy(_proposalID); // Record the winning index for reward payments setUint(defeatIndexKey, _index); } /// @notice Called by a challenger to claim bonds (both refunded bonds and any rewards paid minus the 20% bond burn) /// @param _proposalID The ID of the proposal /// @param _indices An array of indices which the challenger has a claim against function claimBondChallenger(uint256 _proposalID, uint256[] calldata _indices) external onlyLatestContract("rocketDAOProtocolVerifier", address(this)) onlyRegisteredNode(msg.sender) { { // Scope to prevent stack too deep // Check whether the proposal is NOT on the Pending state RocketDAOProtocolProposalInterface daoProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); RocketDAOProtocolProposalInterface.ProposalState proposalState = daoProposal.getState(_proposalID); require(proposalState != RocketDAOProtocolProposalInterface.ProposalState.Pending, "Can not claim bond while proposal is Pending"); } // Check whether the proposal was defeated uint256 defeatIndex = getUint(bytes32(uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID)))+defeatIndexOffset)); bool defeated = defeatIndex != 0; // Keep track of the number of indices the claimer had which were involved in defeating the proposal uint256 rewardedIndices = 0; for (uint256 i = 0; i < _indices.length; ++i) { bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _indices[i])); uint256 challengeData = getUint(challengeKey); Types.ChallengeState challengeState = _getChallengeState(challengeData); if (defeated) { // Refund all challenges if the proposal was defeated require(challengeState == Types.ChallengeState.Responded || challengeState == Types.ChallengeState.Challenged, "Invalid challenge state"); } else { // Only refund non-responded challenges if the proposal wasn't defeated require(challengeState == Types.ChallengeState.Challenged, "Invalid challenge state"); } // Check the challenger is the caller address challenger = address(uint160(challengeData >> addressOffset)); require(msg.sender == challenger, "Invalid challenger"); // Increment reward indices if required if (isRewardedIndex(defeatIndex, _indices[i])) { rewardedIndices++; } // Mark index as paid challengeData = setChallengeState(challengeData, Types.ChallengeState.Paid); setUint(challengeKey, challengeData); } // Get staking contract RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); // Unlock challenger bond uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); uint256 challengeBond = getUint(bytes32(proposalKey + challengeBondOffset)); uint256 totalBond = _indices.length * challengeBond; rocketNodeStaking.unlockRPL(msg.sender, totalBond); // Pay challenger their reward if (rewardedIndices > 0) { uint256 proposalBond = getUint(bytes32(proposalKey + proposalBondOffset)); // Calculate the number of challenges involved in defeating the proposal uint256 nodeCount = getUint(bytes32(proposalKey + nodeCountOffset)); uint256 totalDefeatingIndices = getRoundsFromIndex(defeatIndex, nodeCount); uint256 totalReward = proposalBond * rewardedIndices / totalDefeatingIndices; uint256 burnAmount = totalReward * bondBurnPercent / calcBase; // Unlock the reward amount from the proposer and transfer it to the challenger address proposer = getAddress(bytes32(proposalKey + proposerOffset)); rocketNodeStaking.unlockRPL(proposer, totalReward); rocketNodeStaking.burnRPL(proposer, burnAmount); rocketNodeStaking.transferRPL(proposer, msg.sender, totalReward - burnAmount); } } /// @notice Called by a proposer to claim bonds (both refunded bond and any rewards paid minus the 20% bond burn) /// @param _proposalID The ID of the proposal /// @param _indices An array of indices which the proposer has a claim against function claimBondProposer(uint256 _proposalID, uint256[] calldata _indices) external onlyLatestContract("rocketDAOProtocolVerifier", address(this)) onlyRegisteredNode(msg.sender) { uint256 defeatIndex = getUint(bytes32(uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID)))+defeatIndexOffset)); // Proposer has nothing to claim if their proposal was defeated require(defeatIndex == 0, "Proposal defeated"); // Check the proposal has passed the waiting period and the voting period and wasn't cancelled { RocketDAOProtocolProposalInterface daoProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); RocketDAOProtocolProposalInterface.ProposalState proposalState = daoProposal.getState(_proposalID); require(proposalState >= RocketDAOProtocolProposalInterface.ProposalState.QuorumNotMet, "Invalid proposal state"); } address proposer; uint256 challengeBond; uint256 proposalBond; { uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); proposer = getAddress(bytes32(proposalKey + proposerOffset)); // Only the proposer can call require(msg.sender == proposer, "Not proposer"); // Query proposal bond params challengeBond = getUint(bytes32(proposalKey + challengeBondOffset)); proposalBond = getUint(bytes32(proposalKey + proposalBondOffset)); } // Get staking contract RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); uint256 burnPerChallenge = challengeBond * bondBurnPercent / calcBase; for (uint256 i = 0; i < _indices.length; ++i) { // Check the challenge of the given index was responded to bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _indices[i])); uint256 state = getUint(challengeKey); // Proposer can only claim the reward on indices they responded to require(_getChallengeState(state) == Types.ChallengeState.Responded, "Invalid challenge state"); // Mark index as paid state = setChallengeState(state, Types.ChallengeState.Paid); setUint(challengeKey, state); // If claiming the root at this stage, then we return the proposal bond if (_indices[i] == 1) { rocketNodeStaking.unlockRPL(proposer, proposalBond); } else { // Unlock the challenger bond and pay to proposer address challenger = getChallengeAddress(state); rocketNodeStaking.unlockRPL(challenger, challengeBond); rocketNodeStaking.transferRPL(challenger, proposer, challengeBond - burnPerChallenge); rocketNodeStaking.burnRPL(challenger, burnPerChallenge); } } } /// @notice Used by a proposer to defend a challenged index /// @param _proposalID The ID of the proposal /// @param _index The global index of the node for which the proposer is submitting a new pollard /// @param _nodes A list of nodes making up the new pollard function submitRoot(uint256 _proposalID, uint256 _index, Types.Node[] calldata _nodes) external onlyLatestContract("rocketDAOProtocolVerifier", address(this)) onlyRegisteredNode(msg.sender) { uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); { // Scope to prevent stack too deep // Check whether the proposal is in the Pending state RocketDAOProtocolProposalInterface daoProposal = RocketDAOProtocolProposalInterface(getContractAddress("rocketDAOProtocolProposal")); RocketDAOProtocolProposalInterface.ProposalState proposalState = daoProposal.getState(_proposalID); require(proposalState == RocketDAOProtocolProposalInterface.ProposalState.Pending, "Can not submit root for a valid proposal"); address proposer = getAddress(bytes32(proposalKey + proposerOffset)); require(msg.sender == proposer, "Not proposer"); } { // Scope to prevent stack too deep // Get challenge state bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index)); uint256 state = getUint(challengeKey); // Make sure this index was actually challenged require(_getChallengeState(state) == Types.ChallengeState.Challenged, "Challenge does not exist"); // Mark the index as responded state = setChallengeState(state, Types.ChallengeState.Responded); setUint(challengeKey, state); } // Check the proposal hasn't already been defeated require(getUint(bytes32(proposalKey + defeatIndexOffset)) == 0, "Proposal already defeated"); // Verify correct number of nodes in the pollard uint256 nodeCount = getUint(bytes32(proposalKey + nodeCountOffset)); uint256 indexDepth = Math.log2(_index, Math.Rounding.Down); require(_nodes.length == 2 ** (getNextDepth(_index, nodeCount) - indexDepth), "Invalid node count"); Types.Node memory expected = getNode(_proposalID, _index); Types.Node memory actual = computeRootFromNodes(_nodes); // Check that the supplied nodes sum to the expected value require(expected.sum == actual.sum, "Invalid sum"); // Determine if this index is a leaf node of the primary tree or sub tree { uint256 treeDepth = Math.log2(nodeCount, Math.Rounding.Up); // Verify sub-tree leaves with known values if (indexDepth + depthPerRound >= treeDepth * 2) { // Calculate the offset into the leaf nodes in the final tree that match the supplied nodes uint256 offset = (_index * (2 ** (getNextDepth(_index, nodeCount) - indexDepth))) - (2 ** (treeDepth * 2)); // Verify the leaves match the values we know on chain require(verifyLeaves(getUint(bytes32(proposalKey + blockNumberOffset)), nodeCount, offset, _nodes), "Invalid leaves"); } if (indexDepth == treeDepth) { // The leaf node of the primary tree is just a hash of the sum bytes32 actualHash = keccak256(abi.encodePacked(actual.sum)); require(expected.hash == actualHash, "Invalid hash"); // Update the node to include the root hash of the sub tree setNode(_proposalID, _index, actual); } else { require(expected.hash == actual.hash, "Invalid hash"); } } // Emit event emit RootSubmitted(_proposalID, getAddress(bytes32(proposalKey + proposerOffset)), uint32(getUint(bytes32(proposalKey + blockNumberOffset))), _index, actual, _nodes, block.timestamp); } /// @dev Checks a slice of the final nodes in a tree with the correct known on-chain values /// @param _blockNumber The block number used to generate the voting power tree /// @param _nodeCount The number of nodes that existed at the proposal block /// @param _offset The pollard's offset into the leaves /// @param _leaves The pollard's leaves /// @return True if the leaves match what is known on chain function verifyLeaves(uint256 _blockNumber, uint256 _nodeCount, uint256 _offset, Types.Node[] calldata _leaves) internal view returns (bool) { // Get contracts RocketNetworkVotingInterface rocketNetworkVoting = RocketNetworkVotingInterface(getContractAddress("rocketNetworkVoting")); RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); // Calculate the closest power of 2 of the node count uint256 nodeCount = 2 ** Math.log2(_nodeCount, Math.Rounding.Up); uint32 blockNumber32 = uint32(_blockNumber); // Iterate over the leaves for (uint256 i = 0; i < _leaves.length; ++i) { // The leaf nodes are a 2d array of voting power in the form of [delegateIndex][nodeIndex] where both // arrays are padded out to the closest power of 2 with zeros uint256 nodeIndex = (_offset + i) % nodeCount; uint256 delegateIndex = (_offset + i) / nodeCount; // Determine the correct voting power for this leaf (fill with zero if > node count) uint256 actual = 0; if (nodeIndex < _nodeCount && delegateIndex < _nodeCount) { // Calculate the node and the delegate referred to by this leaf node address nodeAddress = rocketNodeManager.getNodeAt(nodeIndex); address actualDelegate = rocketNetworkVoting.getDelegate(nodeAddress, blockNumber32); // If a delegation exists, retrieve the node's voting power if (actualDelegate == rocketNodeManager.getNodeAt(delegateIndex)) { actual = rocketNetworkVoting.getVotingPower(nodeAddress, blockNumber32); } } // Check provided leaves against actual sum if (_leaves[i].sum != actual) { return false; } // Check provided leaves against hash if (_leaves[i].hash != keccak256(abi.encodePacked(actual))) { return false; } } return true; } /// @notice Check if a vote is valid using a provided proof /// @param _voter address of the node operator casting the vote /// @param _nodeIndex index of the voting node /// @param _proposalID ID of the proposal being voted /// @param _votingPower VP being used with this vote /// @param _witness A merkle proof that will be verified function verifyVote(address _voter, uint256 _nodeIndex, uint256 _proposalID, uint256 _votingPower, Types.Node[] calldata _witness) external view returns (bool) { // Get contracts RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); // Verify voter if(rocketNodeManager.getNodeAt(_nodeIndex) != _voter) { return false; } // Load the proposal uint256 proposalKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal", _proposalID))); // Calculate the network tree index for this voter uint256 nodeCount = getUint(bytes32(proposalKey + nodeCountOffset)); uint256 depth = getMaxDepth(nodeCount); uint256 treeIndex = (2 ** depth) + _nodeIndex; // Reconstruct leaf node Types.Node memory leaf; leaf.sum = _votingPower; leaf.hash = keccak256(abi.encodePacked(_votingPower)); // Retrieve the expected root node Types.Node memory expected = getNode(_proposalID, 1); // Compute a root from the supplied proof Types.Node memory actual = computeRootFromWitness(treeIndex, leaf, _witness); // Equality check return (actual.sum == expected.sum && actual.hash == expected.hash); } /// @dev Computes the root node given a witness /// @param _index The global index the proof is for /// @param _leaf The node at the global index `_index` /// @param _witness A merkle proof starting at the global index `_index` /// @return The computed root node for the given witness function computeRootFromWitness(uint256 _index, Types.Node memory _leaf, Types.Node[] calldata _witness) internal pure returns (Types.Node memory) { Types.Node memory root = _leaf; for (uint256 i = 0; i < _witness.length; ++i) { if (_index % 2 == 1) { root.hash = keccak256(abi.encodePacked( _witness[i].hash, _witness[i].sum, root.hash, root.sum )); } else { root.hash = keccak256(abi.encodePacked( root.hash, root.sum, _witness[i].hash, _witness[i].sum )); } root.sum += _witness[i].sum; _index = _index / 2; } return root; } /// @dev Computes the root node given a pollard /// @param _nodes An array of nodes to compute a root node for /// @return The computed root node function computeRootFromNodes(Types.Node[] calldata _nodes) internal pure returns (Types.Node memory) { uint256 len = _nodes.length / 2; // Perform first step into a new temporary memory buffer to leave original intact Types.Node[] memory temp = new Types.Node[](len); for (uint256 i = 0; i < len; ++i) { temp[i].hash = keccak256(abi.encodePacked( _nodes[i * 2].hash, _nodes[i * 2].sum, _nodes[i * 2 + 1].hash, _nodes[i * 2 + 1].sum )); temp[i].sum = _nodes[i * 2].sum + _nodes[i * 2 + 1].sum; } // Compute the remainder within the temporary buffer while (len > 1) { len /= 2; for (uint256 i = 0; i < len; ++i) { temp[i].hash = keccak256(abi.encodePacked( temp[i * 2].hash, temp[i * 2].sum, temp[i * 2 + 1].hash, temp[i * 2 + 1].sum )); temp[i].sum = temp[i * 2].sum + temp[i * 2 + 1].sum; } } return temp[0]; } /// @dev Calculates the depth of a given index /// @param _index The global index to calculate a depth for /// @return The depth of the global index `_index` function getDepthFromIndex(uint256 _index) internal pure returns (uint256) { return Math.log2(_index, Math.Rounding.Down); } /// @dev Calculates the number of rounds required to get to given index /// @param _index The global index to calculate number of rounds for /// @return The number of rounds it takes to get to the global index `_index` function getRoundsFromIndex(uint256 _index, uint256 _nodeCount) internal pure returns (uint256) { uint256 subTreeDepth = Math.log2(_nodeCount, Math.Rounding.Up); uint256 indexDepth = Math.log2(_index, Math.Rounding.Down); if (indexDepth <= subTreeDepth) { return (indexDepth - 1) / depthPerRound + 1; } else { uint256 phase2Depth = indexDepth - subTreeDepth; uint256 phase1Rounds = (subTreeDepth - 1) / depthPerRound + 1; uint256 phase2Rounds = (phase2Depth - 1) / depthPerRound + 1; return phase1Rounds + phase2Rounds; } } /// @dev Calculates the max depth of a tree containing specified number of nodes /// @param _nodeCount The number of nodes /// @return The max depth of a tree with `_nodeCount` many nodes function getMaxDepth(uint256 _nodeCount) internal pure returns (uint256) { return Math.log2(_nodeCount, Math.Rounding.Up); } /// @dev Calculates the depth of the next round taking into account the max depth /// @param _currentIndex The index to calculate the next depth for /// @param _nodeCount The number of nodes /// @return The next depth for a challenge function getNextDepth(uint256 _currentIndex, uint256 _nodeCount) internal pure returns (uint256) { uint256 currentDepth = getDepthFromIndex(_currentIndex); uint256 maxDepth = getMaxDepth(_nodeCount); uint256 nextDepth = currentDepth + depthPerRound; if (nextDepth > maxDepth * 2) { return maxDepth * 2; } else if (nextDepth > maxDepth) { if (currentDepth < maxDepth) { return maxDepth; } } return nextDepth; } /// @dev Calculates the root index of a pollard given the index of of one of its nodes /// @param _index The index to calculate a pollard root index from /// @return The pollard root index for node with global index of `_index` function getPollardRootIndex(uint256 _index, uint256 _nodeCount) internal pure returns (uint256) { require(_index > 1, "Invalid index"); uint256 indexDepth = Math.log2(_index, Math.Rounding.Down); uint256 maxDepth = Math.log2(_nodeCount, Math.Rounding.Up); if (indexDepth < maxDepth) { // Index is leaf of phase 1 tree uint256 remainder = indexDepth % depthPerRound; require(remainder == 0, "Invalid index"); return _index / (2 ** depthPerRound); } else if (indexDepth == maxDepth) { // Index is a network tree leaf uint256 remainder = indexDepth % depthPerRound; return _index / (2 ** (remainder == 0 ? depthPerRound : remainder)); } else if (indexDepth < maxDepth * 2) { // Index is phase 2 pollard uint256 subIndexDepth = indexDepth - maxDepth; uint256 remainder = subIndexDepth % depthPerRound; require(remainder == 0, "Invalid index"); return _index / (2 ** depthPerRound); } revert("Invalid index"); } /// @dev Returns true if the given `_index` is in the path from the proposal root down to `_defeatIndex` /// @param _defeatIndex The index which resulted in the defeat of the proposal /// @param _index The index to check if it's within the defeat path /// @return True if `_index` was part of the path which defeated the proposal function isRewardedIndex(uint256 _defeatIndex, uint256 _index) internal pure returns (bool) { for (uint256 i = _defeatIndex; i > 1; i /= 2) { if (_index == i) { return true; } } return false; } /// @notice Returns the state of the given challenge /// @param _proposalID The ID of the proposal the challenge is for /// @param _index The global index of the node that is challenged /// @return The state of the challenge for the given proposal and node function getChallengeState(uint256 _proposalID, uint256 _index) override external view returns (Types.ChallengeState) { bytes32 challengeKey = keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index)); uint256 data = getUint(challengeKey); return _getChallengeState(data); } /// @dev Extracts the packed challenge state from the given uint256 function _getChallengeState(uint256 _data) internal pure returns (Types.ChallengeState) { return Types.ChallengeState(uint8(_data >> stateOffset)); } /// @dev Extracts the packed timestamp from the given uint256 function getChallengeTimestamp(uint256 _data) internal pure returns (uint64) { return uint64(_data >> timestampOffset); } /// @dev Extracts the packed address of the challenger from the given uint256 function getChallengeAddress(uint256 _data) internal pure returns (address) { return address(uint160(_data >> addressOffset)); } /// @dev Modifies the packed challenge state of a given uint256 function setChallengeState(uint256 _data, Types.ChallengeState _newState) internal pure returns (uint256) { _data &= ~(uint256(~uint8(0)) << stateOffset); _data |= uint256(_newState) << stateOffset; return _data; } /// @notice Retrieves the sum and hash of the node at the given global index function getNode(uint256 _proposalID, uint256 _index) public view returns (Types.Node memory) { uint256 challengeKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index))); Types.Node memory node; node.sum = getUint(bytes32(challengeKey + sumOffset)); node.hash = getBytes32(bytes32(challengeKey + hashOffset)); return node; } /// @dev Sets the sum and hash of the node at the given global index function setNode(uint256 _proposalID, uint256 _index, Types.Node memory _node) internal { uint256 challengeKey = uint256(keccak256(abi.encodePacked("dao.protocol.proposal.challenge", _proposalID, _index))); setUint(bytes32(challengeKey + sumOffset), _node.sum); setBytes32(bytes32(challengeKey + hashOffset), _node.hash); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettings.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettingsInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsInterface.sol"; import {RocketBase} from "../../../RocketBase.sol"; // Settings in RP which the DAO will have full control over // This settings contract enables storage using setting paths with namespaces, rather than explicit set methods abstract contract RocketDAOProtocolSettings is RocketBase, RocketDAOProtocolSettingsInterface { // The namespace for a particular group of settings bytes32 settingNameSpace; // Only allow updating from the DAO proposals contract modifier onlyDAOProtocolProposal() { // If this contract has been initialised, only allow access from the proposals contract if (getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) require(getContractAddress("rocketDAOProtocolProposals") == msg.sender, "Only DAO Protocol Proposals contract can update a setting"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress, string memory _settingNameSpace) RocketBase(_rocketStorageAddress) { // Apply the setting namespace settingNameSpace = keccak256(abi.encodePacked("dao.protocol.setting.", _settingNameSpace)); } /*** Uints ****************/ // A general method to return any setting given the setting path is correct, only accepts uints function getSettingUint(string memory _settingPath) public view override returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); } // Update a Uint setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingUint(string memory _settingPath, uint256 _value) virtual public override onlyDAOProtocolProposal { // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /*** Bools ****************/ // A general method to return any setting given the setting path is correct, only accepts bools function getSettingBool(string memory _settingPath) public view override returns (bool) { return getBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); } // Update a setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingBool(string memory _settingPath, bool _value) virtual public override onlyDAOProtocolProposal { // Update setting now setBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /*** Addresses ****************/ // A general method to return any setting given the setting path is correct, only accepts addresses function getSettingAddress(string memory _settingPath) external view override returns (address) { return getAddress(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); } // Update a setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingAddress(string memory _settingPath, address _value) virtual external override onlyDAOProtocolProposal { // Update setting now setAddress(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /*** Address lists ****************/ // A general method to return any setting given the setting path is correct, only accepts address lists function getSettingAddressList(string memory _settingPath) public view override returns (address[] memory) { uint256 key = uint256(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); uint256 count = getUint(bytes32(key)); address[] memory addressList = new address[](count); for (uint256 i = 0; i < count; ++i) { addressList[i] = getAddress(bytes32(key + i)); } return addressList; } // Update a setting, can only be executed by the DAO contract when a majority on a setting proposal has passed and been executed function setSettingAddressList(string memory _settingPath, address[] calldata _value) virtual public override onlyDAOProtocolProposal { uint256 key = uint256(keccak256(abi.encodePacked(settingNameSpace, _settingPath))); setUint(bytes32(key), _value.length); for (uint256 i = 0; i < _value.length; ++i) { setAddress(bytes32(key + i), _value[i]); } } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsAuctionInterface.sol"; /// @notice Network auction settings contract RocketDAOProtocolSettingsAuction is RocketDAOProtocolSettings, RocketDAOProtocolSettingsAuctionInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "auction") { version = 3; // Initialise settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Apply settings setSettingBool("auction.lot.create.enabled", true); setSettingBool("auction.lot.bidding.enabled", true); setSettingUint("auction.lot.value.minimum", 1 ether); setSettingUint("auction.lot.value.maximum", 10 ether); setSettingUint("auction.lot.duration", 50400); // 7 days setSettingUint("auction.price.start", 1 ether); // 100% setSettingUint("auction.price.reserve", 0.5 ether); // 50% // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @dev Overrides inherited setting method with extra sanity checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(abi.encodePacked(_settingPath)); if(settingKey == keccak256(abi.encodePacked("auction.lot.value.minimum"))) { // >= 1 RPL (RPIP-33) require(_value >= 1 ether, "Value must be >= 1 RPL"); } else if(settingKey == keccak256(abi.encodePacked("auction.lot.value.maximum"))) { // >= 1 RPL (RPIP-33) require(_value >= 1 ether, "Value must be >= 1 RPL"); } else if(settingKey == keccak256(abi.encodePacked("auction.lot.duration"))) { // >= 1 day (RPIP-33) (approximated by blocks) require(_value >= 7200, "Value must be >= 7200"); } else if(settingKey == keccak256(abi.encodePacked("auction.price.start"))) { // >= 10% (RPIP-33) require(_value >= 0.1 ether, "Value must be >= 10%"); } else if(settingKey == keccak256(abi.encodePacked("auction.price.reserve"))) { // >= 10% (RPIP-33) require(_value >= 0.1 ether, "Value must be >= 10%"); } } // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice Lot creation currently enabled function getCreateLotEnabled() override external view returns (bool) { return getSettingBool("auction.lot.create.enabled"); } /// @notice Bidding on lots currently enabled function getBidOnLotEnabled() override external view returns (bool) { return getSettingBool("auction.lot.bidding.enabled"); } /// @notice The minimum lot size relative to ETH value function getLotMinimumEthValue() override external view returns (uint256) { return getSettingUint("auction.lot.value.minimum"); } /// @notice The maximum lot size relative to ETH value function getLotMaximumEthValue() override external view returns (uint256) { return getSettingUint("auction.lot.value.maximum"); } /// @notice The maximum auction duration in blocks function getLotDuration() override external view returns (uint256) { return getSettingUint("auction.lot.duration"); } /// @notice The starting price relative to current RPL price, as a fraction of 1 ether function getStartingPriceRatio() override external view returns (uint256) { return getSettingUint("auction.price.start"); } /// @notice The reserve price relative to current RPL price, as a fraction of 1 ether function getReservePriceRatio() override external view returns (uint256) { return getSettingUint("auction.price.reserve"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettingsDepositInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsDepositInterface.sol"; import {RocketBase} from "../../../RocketBase.sol"; import {RocketDAOProtocolSettings} from "./RocketDAOProtocolSettings.sol"; /// @notice Network deposit settings contract RocketDAOProtocolSettingsDeposit is RocketDAOProtocolSettings, RocketDAOProtocolSettingsDepositInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "deposit") { version = 5; // Initialise settings on deployment if (!rocketStorage.getDeployedStatus()) { // Set defaults setSettingBool("deposit.enabled", false); setSettingBool("deposit.assign.enabled", true); _setSettingUint("deposit.minimum", 0.01 ether); _setSettingUint("deposit.pool.maximum", 160 ether); _setSettingUint("deposit.assign.maximum", 90); _setSettingUint("deposit.assign.socialised.maximum", 0); _setSettingUint("deposit.fee", 0.0005 ether); // Set to approx. 1 day of rewards at 18.25% APR _setSettingUint("express.queue.rate", 4); // RPIP-75 _setSettingUint("express.queue.tickets.base.provision", 0); // RPIP-75 // Set deploy flag setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract /// @param _settingPath The path of the setting within this contract's namespace /// @param _value The value to set it to function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(bytes(_settingPath)); if (settingKey == keccak256(bytes("deposit.fee"))) { require(_value < 0.01 ether, "Fee must be less than 1%"); } else if (settingKey == keccak256(bytes("express.queue.rate"))) { require(_value > 0, "Rate must be greater than 0"); } } // Update setting now _setSettingUint(_settingPath, _value); } /// @dev Directly updates a setting, no guardrails applied function _setSettingUint(string memory _settingPath, uint256 _value) internal { setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice Returns true if deposits are currently enabled function getDepositEnabled() override external view returns (bool) { return getSettingBool("deposit.enabled"); } /// @notice Returns true if deposit assignments are currently enabled function getAssignDepositsEnabled() override external view returns (bool) { return getSettingBool("deposit.assign.enabled"); } /// @notice Returns the minimum deposit size function getMinimumDeposit() override external view returns (uint256) { return getSettingUint("deposit.minimum"); } /// @notice Returns the maximum size of the deposit pool function getMaximumDepositPoolSize() override external view returns (uint256) { return getSettingUint("deposit.pool.maximum"); } /// @notice Returns the maximum number of deposit assignments to perform at once function getMaximumDepositAssignments() override external view returns (uint256) { return getSettingUint("deposit.assign.maximum"); } /// @notice Returns the maximum number of socialised (ie, not related to deposit size) assignments to perform function getMaximumDepositSocialisedAssignments() override external view returns (uint256) { return getSettingUint("deposit.assign.socialised.maximum"); } /// @notice Returns the current fee paid on user deposits function getDepositFee() override external view returns (uint256) { return getSettingUint("deposit.fee"); } /// @notice Returns the rate at which the express queue processes over the normal queue function getExpressQueueRate() override external view returns (uint256) { return getSettingUint("express.queue.rate"); } /// @notice Returns the number of express queue tickets a new node operator receives function getExpressQueueTicketsBaseProvision() override external view returns (uint256) { return getSettingUint("express.queue.tickets.base.provision"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsInflation.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsInflationInterface.sol"; import "../../../../interface/token/RocketTokenRPLInterface.sol"; /// @notice RPL Inflation settings in RP which the DAO will have full control over contract RocketDAOProtocolSettingsInflation is RocketDAOProtocolSettings, RocketDAOProtocolSettingsInflationInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "inflation") { version = 2; // Set some initial settings on first deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // RPL Inflation settings setSettingUint("rpl.inflation.interval.rate", 1000133680617113500); // 5% annual calculated on a daily interval - Calculate in js example: let dailyInflation = web3.utils.toBN((1 + 0.05) ** (1 / (365)) * 1e18); setSettingUint("rpl.inflation.interval.start", block.timestamp + 1 days); // Set the default start date for inflation to begin as 1 day after deployment // Deployment check setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); // Flag that this contract has been deployed, so default settings don't get reapplied on a contract upgrade } } /*** Set Uint *****************************************/ /// @notice Update a setting, overrides inherited setting method with extra checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings // The start time for inflation must be in the future and cannot be set again once started if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(bytes(_settingPath)); if(settingKey == keccak256(bytes("rpl.inflation.interval.start"))) { // Must be a future timestamp require(_value > block.timestamp, "Inflation interval start time must be in the future"); // If it's already set and started, a new start block cannot be set if(getInflationIntervalStartTime() > 0) { require(getInflationIntervalStartTime() > block.timestamp, "Inflation has already started"); } } else if(settingKey == keccak256(bytes("rpl.inflation.interval.rate"))) { // No greater than 1e16 more than the previous value. (RPIP-33) require(_value <= getSettingUint("rpl.inflation.interval.rate") + 0.01 ether, "No greater than 1e16 more than the previous value"); require(_value >= 1, "Inflation can't be negative"); // RPL contract address address rplContractAddress = getContractAddressUnsafe("rocketTokenRPL"); if(rplContractAddress != address(0x0)) { // Force inflation at old rate before updating inflation rate RocketTokenRPLInterface rplContract = RocketTokenRPLInterface(rplContractAddress); // Mint any new tokens from the RPL inflation rplContract.inflationMintTokens(); } } } // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /*** RPL Contract Settings *****************************************/ /// @notice RPL yearly inflation rate per interval (daily by default) function getInflationIntervalRate() override external view returns (uint256) { return getSettingUint("rpl.inflation.interval.rate"); } /// @notice The block to start inflation at function getInflationIntervalStartTime() override public view returns (uint256) { return getSettingUint("rpl.inflation.interval.start"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMegapool.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettingsMegapoolInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMegapoolInterface.sol"; import {RocketBase} from "../../../RocketBase.sol"; import {RocketDAOProtocolSettings} from "./RocketDAOProtocolSettings.sol"; /// @notice Network megapool settings contract RocketDAOProtocolSettingsMegapool is RocketDAOProtocolSettings, RocketDAOProtocolSettingsMegapoolInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "megapool") { version = 1; // Initialise settings on deployment if (!rocketStorage.getDeployedStatus()) { initialise(); } } /// @notice Called during deployment or upgrade to set initial values for settings function initialise() override public { // Set defaults _setSettingUint("megapool.time.before.dissolve", 28 days); // Time that must be waited before dissolving a megapool validator (RPIP-59) _setSettingUint("megapool.dissolve.penalty", 0.05 ether); // The penalty which is applied to node operators when one of their validators gets dissolved _setSettingUint("maximum.megapool.eth.penalty", 612 ether); // Maximum ETH penalty that can be applied over a rolling 7-day window (RPIP-42) _setSettingUint("notify.threshold", 112); // Number of epochs before withdrawable_epoch a node operator must notify exit (RPIP-72) _setSettingUint("late.notify.fine", 0.05 ether); // Fine applied to node operator for not notifying exit in time (RPIP-72) _setSettingUint("user.distribute.delay", 1575); // How many epochs a user must wait before distributing someone else's megapool (RPIP-72) _setSettingUint("user.distribute.delay.shortfall", 6750); // How many epochs a user must wait before distributing someone else's megapool with a shortfall of user funds _setSettingUint("megapool.penalty.threshold", 0.51 ether); // Percentage of trusted members that must vote in favour of a penalty // Update deploy flag require (!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed"))), "Already initialised"); setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract /// @param _settingPath The path of the setting within this contract's namespace /// @param _value The value to set it to function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Some safety guards for certain settings bytes32 settingKey = keccak256(abi.encodePacked(_settingPath)); if (settingKey == keccak256(bytes("megapool.time.before.dissolve"))) { require(_value >= 10 days && _value <= 60 days, "Value must be >= 10 days & <= 60 days"); } else if (settingKey == keccak256(bytes("maximum.megapool.eth.penalty"))) { require(_value >= 300 ether && _value <= 5000 ether, "Value must be >= 300 ETH & <= 5000 ETH"); } else if (settingKey == keccak256(bytes("notify.threshold"))) { require(_value >= 19 && _value <= 456, "Value must be >= 19 epochs & <= 456 epochs"); } else if (settingKey == keccak256(bytes("late.notify.fine"))) { require(_value >= 0.01 ether && _value <= 0.5 ether, "Value must be >= 0.01 ETH & <= 0.5 ETH"); } else if (settingKey == keccak256(bytes("user.distribute.delay"))) { require(_value >= 225 && _value <= 13500, "Value must be >= 225 & <= 13500 epochs"); require(_value <= getSettingUint("user.distribute.delay.shortfall"), "Value must be <= user.distribute.delay.shortfall"); } else if (settingKey == keccak256(bytes("user.distribute.delay.shortfall"))) { require(_value >= 6750 && _value <= 40500, "Value must be >= 6750 & <= 40500 epochs"); require(_value >= getSettingUint("user.distribute.delay"), "Value must be >= user.distribute.delay"); } else if (settingKey == keccak256(bytes("megapool.penalty.threshold"))) { require(_value >= 0.51 ether, "Penalty threshold must be 51% or higher"); } else if (settingKey == keccak256(bytes("megapool.dissolve.penalty"))) { require(_value >= 0.01 ether, "Value must be >= 0.01 ETH"); } } // Update setting now _setSettingUint(_settingPath, _value); } /// @dev Directly updates a setting, no guardrails applied function _setSettingUint(string memory _settingPath, uint256 _value) internal { setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice Returns how long after an assignment a watcher must wait to dissolve a megapool validator (seconds) function getTimeBeforeDissolve() override external view returns (uint256) { return getSettingUint("megapool.time.before.dissolve"); } /// @notice Returns the penalty applied to a NO for having a validator dissolved function getDissolvePenalty() override external view returns (uint256) { return getSettingUint("megapool.dissolve.penalty"); } /// @notice Returns the maximum amount megapools can be penalised in a 7 day rolling window function getMaximumEthPenalty() override external view returns (uint256) { return getSettingUint("maximum.megapool.eth.penalty"); } /// @notice Returns the amount of time before `withdrawable_epoch` a node operator must notify their exit function getNotifyThreshold() override external view returns (uint256) { return getSettingUint("notify.threshold"); } /// @notice Returns the amount a node operator is fined for notifying their exit late function getLateNotifyFine() override external view returns (uint256) { return getSettingUint("late.notify.fine"); } /// @notice Returns the number of epochs a user must wait before distributing another node's megapool function getUserDistributeDelay() override external view returns (uint256) { return getSettingUint("user.distribute.delay"); } /// @notice Returns the number of epochs a user must wait before distributing another node's megapool if the distribute results in a shortfall of user funds function getUserDistributeDelayWithShortfall() override external view returns (uint256) { return getSettingUint("user.distribute.delay.shortfall"); } /// @notice Returns the percentage of trusted members that must vote in favour of a penalty function getPenaltyThreshold() override external view returns (uint256) { return getSettingUint("megapool.penalty.threshold"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import "../../../../types/MinipoolDeposit.sol"; /// @notice Network minipool settings contract RocketDAOProtocolSettingsMinipool is RocketDAOProtocolSettings, RocketDAOProtocolSettingsMinipoolInterface { uint256 constant internal minipoolUserDistributeWindowStart = 90 days; constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "minipool") { version = 4; // Initialise settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Apply settings setSettingBool("minipool.submit.withdrawable.enabled", false); setSettingBool("minipool.bond.reduction.enabled", false); setSettingUint("minipool.launch.timeout", 72 hours); setSettingUint("minipool.maximum.count", 14); setSettingUint("minipool.user.distribute.window.length", 2 days); setSettingUint("minipool.maximum.penalty.count", 2500); // Max number of penalties oDAO can apply in rolling 1 week window (RPIP-52) // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract /// @param _settingPath The path of the setting within this contract's namespace /// @param _value The value to set it to function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(abi.encodePacked(_settingPath)); if(settingKey == keccak256(bytes("minipool.launch.timeout"))) { RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); require(_value >= (rocketDAONodeTrustedSettingsMinipool.getScrubPeriod() + 1 hours), "Launch timeout must be greater than scrub period"); require(_value >= 12 hours, "Launch timeout must be greater than 12 hours"); } else if(settingKey == keccak256(bytes("minipool.maximum.penalty.count"))) { require(_value >= 1000 && _value <= 5000, "Value must be >= 1000 & <= 5000"); } } // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice Returns the balance required to launch minipool function getLaunchBalance() override public pure returns (uint256) { return 32 ether; } /// @notice Returns the value required to pre-launch a minipool function getPreLaunchValue() override public pure returns (uint256) { return 1 ether; } /// @notice Returns the deposit amount for a given deposit type (only used for legacy minipool types) function getDepositUserAmount(MinipoolDeposit _depositType) override external pure returns (uint256) { if (_depositType == MinipoolDeposit.Full) { return getFullDepositUserAmount(); } if (_depositType == MinipoolDeposit.Half) { return getHalfDepositUserAmount(); } return 0; } /// @notice Returns the user amount for a "Full" deposit minipool function getFullDepositUserAmount() override public pure returns (uint256) { return getLaunchBalance() / 2; } /// @notice Returns the user amount for a "Half" deposit minipool function getHalfDepositUserAmount() override public pure returns (uint256) { return getLaunchBalance() / 2; } /// @notice Returns the amount a "Variable" minipool requires to move to staking status function getVariableDepositAmount() override external pure returns (uint256) { return getLaunchBalance() - getPreLaunchValue(); } /// @notice Submit minipool withdrawable events currently enabled (trusted nodes only) function getSubmitWithdrawableEnabled() override external view returns (bool) { return getSettingBool("minipool.submit.withdrawable.enabled"); } /// @notice Returns true if bond reductions are currentl enabled function getBondReductionEnabled() override external view returns (bool) { return getSettingBool("minipool.bond.reduction.enabled"); } /// @notice Returns the timeout period in seconds for prelaunch minipools to launch function getLaunchTimeout() override external view returns (uint256) { return getSettingUint("minipool.launch.timeout"); } /// @notice Returns the maximum number of minipools allowed at one time function getMaximumCount() override external view returns (uint256) { return getSettingUint("minipool.maximum.count"); } /// @notice Returns true if the given time is within the user distribute window function isWithinUserDistributeWindow(uint256 _time) override external view returns (bool) { uint256 start = getUserDistributeWindowStart(); uint256 length = getUserDistributeWindowLength(); return (_time >= start && _time < (start + length)); } /// @notice Returns true if the given time has passed the distribute window function hasUserDistributeWindowPassed(uint256 _time) override external view returns (bool) { uint256 start = getUserDistributeWindowStart(); uint256 length = getUserDistributeWindowLength(); return _time >= start + length; } /// @notice Returns the start of the user distribute window function getUserDistributeWindowStart() override public pure returns (uint256) { return minipoolUserDistributeWindowStart; } /// @notice Returns the length of the user distribute window function getUserDistributeWindowLength() override public view returns (uint256) { return getSettingUint("minipool.user.distribute.window.length"); } /// @notice Returns the maximum number of penalties the oDAO can apply in a rolling 1 week window function getMaximumPenaltyCount() override external view returns (uint256) { return getSettingUint("minipool.maximum.penalty.count"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNetwork.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketNetworkRevenuesInterface} from "../../../../interface/network/RocketNetworkRevenuesInterface.sol"; import {RocketBase} from "../../../RocketBase.sol"; import {RocketDAOProtocolSettings} from "./RocketDAOProtocolSettings.sol"; /// @notice Network auction settings contract RocketDAOProtocolSettingsNetwork is RocketDAOProtocolSettings, RocketDAOProtocolSettingsNetworkInterface { // Modifiers modifier onlyAllowListedController() { require(isAllowListedController(msg.sender), "Not on allow list"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "network") { version = 4; // Initialise settings on deployment if (!rocketStorage.getDeployedStatus()) { // Set defaults _setSettingUint("network.consensus.threshold", 0.51 ether); // 51% _setSettingBool("network.submit.balances.enabled", true); _setSettingUint("network.submit.balances.frequency", 1 days); _setSettingBool("network.submit.prices.enabled", true); _setSettingUint("network.submit.prices.frequency", 1 days); _setSettingUint("network.node.fee.minimum", 0.15 ether); // 15% _setSettingUint("network.node.fee.target", 0.15 ether); // 15% _setSettingUint("network.node.fee.maximum", 0.15 ether); // 15% _setSettingUint("network.node.fee.demand.range", 160 ether); _setSettingUint("network.reth.collateral.target", 0.1 ether); _setSettingUint("network.penalty.threshold", 0.51 ether); // Consensus for penalties requires 51% vote _setSettingUint("network.penalty.per.rate", 0.1 ether); // 10% per penalty _setSettingBool("network.submit.rewards.enabled", true); // Enable reward submission _setSettingUint("network.node.commission.share", 0.05 ether); // 5% (RPIP-46) _setSettingUint("network.node.commission.share.security.council.adder", 0 ether); // 0% (RPIP-46) _setSettingUint("network.voter.share", 0.09 ether); // 9% (RPIP-46) _setSettingUint("network.pdao.share", 0.00 ether); // 0% (RPIP-72) _setSettingUint("network.max.node.commission.share.council.adder", 0.01 ether); // 1% (RPIP-46) _setSettingUint("network.max.reth.balance.delta", 0.02 ether); // 2% (RPIP-61) // Set deploy flag setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { if (getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Some safety guards for certain settings bytes32 settingKey = keccak256(bytes(_settingPath)); if (settingKey == keccak256(bytes("network.consensus.threshold"))) { require(_value >= 0.51 ether, "Consensus threshold must be 51% or higher"); } else if (settingKey == keccak256(bytes("network.node.fee.minimum"))) { require(_value >= 0.05 ether && _value <= 0.2 ether, "The node fee minimum must be a value between 5% and 20%"); } else if (settingKey == keccak256(bytes("network.node.fee.target"))) { require(_value >= 0.05 ether && _value <= 0.2 ether, "The node fee target must be a value between 5% and 20%"); } else if (settingKey == keccak256(bytes("network.node.fee.maximum"))) { require(_value >= 0.05 ether && _value <= 0.2 ether, "The node fee maximum must be a value between 5% and 20%"); } else if (settingKey == keccak256(bytes("network.submit.balances.frequency"))) { require(_value >= 1 hours && _value <= 7 days, "Value must be >= 1 hour & <= 7 days"); } else if (settingKey == keccak256(bytes("network.max.reth.balance.delta"))) { require(_value >= 0.01 ether, "The max rETH balance delta must be >= 1%"); } else if (settingKey == keccak256(bytes("network.reth.collateral.target"))) { require(_value <= 0.5 ether, "Value must be <= 50%"); } else if (settingKey == keccak256(bytes("network.node.commission.share.security.council.adder"))) { return _setNodeShareSecurityCouncilAdder(_value); } else if (settingKey == keccak256(bytes("network.node.commission.share"))) { return _setNodeCommissionShare(_value); } else if (settingKey == keccak256(bytes("network.voter.share"))) { return _setVoterShare(_value); } else if (settingKey == keccak256(bytes("network.pdao.share"))) { return _setProtocolDAOShare(_value); } // Update setting now _setSettingUint(_settingPath, _value); } else { // Update setting now _setSettingUint(_settingPath, _value); } } /// @dev Sets a namespaced uint value skipping any guardrails function _setSettingUint(string memory _settingPath, uint256 _value) internal { setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @dev Sets a namespaced bool value skipping any guardrails function _setSettingBool(string memory _settingPath, bool _value) internal { setBool(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } // @notice Returns the maximum value the security council can set the node share security council adder to function getMaxNodeShareSecurityCouncilAdder() override public view returns (uint256) { return getSettingUint("network.max.node.commission.share.council.adder"); } // @notice Returns the current voter share (excluding security council adder) function getVoterShare() override public view returns (uint256) { return getSettingUint("network.voter.share"); } // @notice Returns the current pdao share function getProtocolDAOShare() override public view returns (uint256) { return getSettingUint("network.pdao.share"); } // @notice Returns the current node share (excluding security council adder) function getNodeShare() override public view returns (uint256) { return getSettingUint("network.node.commission.share"); } // @notice Returns the current node share security council adder function getNodeShareSecurityCouncilAdder() override public view returns (uint256) { return getSettingUint("network.node.commission.share.security.council.adder"); } // @notice Returns the current rETH commission function getRethCommission() override public view returns (uint256) { return getNodeShare() + getVoterShare() + getProtocolDAOShare(); } // @notice Returns the current voter share (taking into account the security council adder) function getEffectiveVoterShare() override public view returns (uint256) { return getVoterShare() - getNodeShareSecurityCouncilAdder(); } // @notice Returns the current node share (taking into account the security council adder) function getEffectiveNodeShare() override public view returns (uint256) { return getNodeShare() + getNodeShareSecurityCouncilAdder(); } /// @notice The threshold of trusted nodes that must reach consensus on oracle data to commit it function getNodeConsensusThreshold() override external view returns (uint256) { return getSettingUint("network.consensus.threshold"); } /// @notice The threshold of trusted nodes that must reach consensus on a penalty function getNodePenaltyThreshold() override external view returns (uint256) { return getSettingUint("network.penalty.threshold"); } /// @notice The amount to penalise a minipool for each feeDistributor infraction function getPerPenaltyRate() override external view returns (uint256) { return getSettingUint("network.penalty.per.rate"); } /// @notice Submit balances currently enabled (trusted nodes only) function getSubmitBalancesEnabled() override external view returns (bool) { return getSettingBool("network.submit.balances.enabled"); } /// @notice The frequency in seconds at which network balances should be submitted by trusted nodes function getSubmitBalancesFrequency() override external view returns (uint256) { return getSettingUint("network.submit.balances.frequency"); } /// @notice Submit prices currently enabled (trusted nodes only) function getSubmitPricesEnabled() override external view returns (bool) { return getSettingBool("network.submit.prices.enabled"); } /// @notice The frequency in seconds at which network prices should be submitted by trusted nodes function getSubmitPricesFrequency() override external view returns (uint256) { return getSettingUint("network.submit.prices.frequency"); } /// @notice The minimum node commission rate as a fraction of 1 ether function getMinimumNodeFee() override external view returns (uint256) { return getSettingUint("network.node.fee.minimum"); } /// @notice The target node commission rate as a fraction of 1 ether function getTargetNodeFee() override external view returns (uint256) { return getSettingUint("network.node.fee.target"); } /// @notice The maximum node commission rate as a fraction of 1 ether function getMaximumNodeFee() override external view returns (uint256) { return getSettingUint("network.node.fee.maximum"); } /// @notice The range of node demand values to base fee calculations on (from negative to positive value) function getNodeFeeDemandRange() override external view returns (uint256) { return getSettingUint("network.node.fee.demand.range"); } /// @notice Target rETH collateralisation rate as a fraction of 1 ether function getTargetRethCollateralRate() override external view returns (uint256) { return getSettingUint("network.reth.collateral.target"); } /// @notice rETH withdraw delay in blocks function getRethDepositDelay() override external view returns (uint256) { return getSettingUint("network.reth.deposit.delay"); } /// @notice Submit reward snapshots currently enabled (trusted nodes only) function getSubmitRewardsEnabled() override external view returns (bool) { return getSettingBool("network.submit.rewards.enabled"); } /// @notice Returns a list of addresses allowed to update commission share parameters function getAllowListedControllers() override public view returns (address[] memory) { return getSettingAddressList("network.allow.listed.controllers"); } /// @notice Returns the maximum amount rETH balance deltas can be changed per submission (as a percentage of 1e18) function getMaxRethDelta() override external view returns (uint256) { return getSettingUint("network.max.reth.balance.delta"); } /// @notice Returns true if the supplied address is one of the allow listed controllers /// @param _address The address to check for on the allow list function isAllowListedController(address _address) override public view returns (bool) { address[] memory allowList = getAllowListedControllers(); for (uint256 i = 0; i < allowList.length; ++i) { if (allowList[i] == _address) return true; } return false; } /// @notice Called by an explicitly allowed address to modify the security council adder parameter /// @param _value New value for the parameter function setNodeShareSecurityCouncilAdder(uint256 _value) override external onlyAllowListedController { _setNodeShareSecurityCouncilAdder(_value); } /// @notice Called by an explicitly allowed address to modify the node commission share parameter /// @param _value New value for the parameter function setNodeCommissionShare(uint256 _value) override external onlyAllowListedController { _setNodeCommissionShare(_value); } /// @notice Called by an explicitly allowed address to modify the voter share parameter /// @param _value New value for the parameter function setVoterShare(uint256 _value) override external onlyAllowListedController { _setVoterShare(_value); } /// @notice Called by an explicitly allowed address to modify the pdao share parameter /// @param _value New value for the parameter function setProtocolDAOShare(uint256 _value) override external onlyAllowListedController { _setProtocolDAOShare(_value); } /// @dev Internal implementation of setting the node share security council adder parameter function _setNodeShareSecurityCouncilAdder(uint256 _value) internal { // Validate input uint256 maxAdderValue = getSettingUint("network.max.node.commission.share.council.adder"); require(_value <= maxAdderValue, "Value must be <= max value"); uint256 maxVoterValue = getSettingUint("network.voter.share"); require(_value <= maxVoterValue, "Value must be <= voter share"); // Make setting change _setSettingUint("network.node.commission.share.security.council.adder", _value); // Sanity check value require(getRethCommission() <= 1 ether, "rETH Commission must be <= 100%"); // Notify change of UARS parameter for snapshot RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); rocketNetworkRevenues.setVoterShare(getEffectiveVoterShare()); rocketNetworkRevenues.setNodeShare(getEffectiveNodeShare()); } /// @dev Internal implementation of setting the node commission share parameter function _setNodeCommissionShare(uint256 _value) internal { // Make setting change _setSettingUint("network.node.commission.share", _value); // Sanity check value require(getRethCommission() <= 1 ether, "rETH Commission must be <= 100%"); // Notify change of UARS parameter for snapshot RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); rocketNetworkRevenues.setNodeShare(getEffectiveNodeShare()); } /// @dev Internal implementation of setting the voter share parameter function _setVoterShare(uint256 _value) internal { // Make setting change _setSettingUint("network.voter.share", _value); // Sanity check value require(getRethCommission() <= 1 ether, "rETH Commission must be <= 100%"); // Notify change of UARS parameter for snapshot RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); rocketNetworkRevenues.setVoterShare(getEffectiveVoterShare()); } /// @dev Internal implementation of setting the pdao share parameter function _setProtocolDAOShare(uint256 _value) internal { // Make setting change _setSettingUint("network.pdao.share", _value); // Sanity check value require(getRethCommission() <= 1 ether, "rETH Commission must be <= 100%"); // Notify change of UARS parameter for snapshot RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); rocketNetworkRevenues.setProtocolDAOShare(_value); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNode.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettings} from "./RocketDAOProtocolSettings.sol"; import {RocketDAOProtocolSettingsNodeInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../../../interface/network/RocketNetworkSnapshotsInterface.sol"; /// @notice Network auction settings contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOProtocolSettingsNodeInterface { uint256 constant internal milliToWei = 10 ** 15; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "node") { // Set version version = 5; // Initialise settings on deployment if (!rocketStorage.getDeployedStatus()) { // Set defaults setSettingBool("node.registration.enabled", false); setSettingBool("node.smoothing.pool.registration.enabled", true); setSettingBool("node.deposit.enabled", false); setSettingBool("node.vacant.minipools.enabled", false); _setSettingUint("reduced.bond", 4 ether); // 4 ETH (RPIP-42) _setSettingUint("node.unstaking.period", 28 days); // 28 days (RPIP-30) _setSettingUint("node.withdrawal.cooldown", 0); // No cooldown (RPIP-30) _setSettingUint("node.minimum.legacy.staked.rpl", 0.15 ether); // 15% of borrowed ETH (RPIP-30) // Update deployed flag setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(bytes(_settingPath)); if(settingKey == keccak256(bytes("node.voting.power.stake.maximum"))) { require(_value >= 1 ether && _value <= 5 ether, "Value must be >= 100% & <= 500%"); // Redirect the setting change to push a new value into the snapshot system instead RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); rocketNetworkSnapshots.push(settingKey, uint224(_value)); return; } else if(settingKey == keccak256(bytes("reduced.bond"))) { require(_value % milliToWei == 0, "Value must be divisible by milliwei"); require(_value >= 1 ether && _value <= 4 ether, "Value must be >= 1 ETH & <= 4 ETH"); } else if(settingKey == keccak256(bytes("node.unstaking.period"))) { require(_value <= 6 weeks, "Value must be <= 6 weeks"); } else if(settingKey == keccak256(bytes("node.withdrawal.cooldown"))) { require(_value <= 6 weeks, "Value must be <= 6 weeks"); } } // Update setting now _setSettingUint(_settingPath, _value); } /// @dev Directly updates a setting, no guardrails applied function _setSettingUint(string memory _settingPath, uint256 _value) internal { setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } // Node registrations currently enabled function getRegistrationEnabled() override external view returns (bool) { return getSettingBool("node.registration.enabled"); } // Node smoothing pool registrations currently enabled function getSmoothingPoolRegistrationEnabled() override external view returns (bool) { return getSettingBool("node.smoothing.pool.registration.enabled"); } // Node deposits currently enabled function getDepositEnabled() override external view returns (bool) { return getSettingBool("node.deposit.enabled"); } // Vacant minipools currently enabled function getVacantMinipoolsEnabled() override external view returns (bool) { return getSettingBool("node.vacant.minipools.enabled"); } // Maximum staked RPL that applies to voting power per minipool as a fraction of assigned user ETH value function getMaximumStakeForVotingPower() override external view returns (uint256) { bytes32 settingKey = keccak256(bytes("node.voting.power.stake.maximum")); RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); return uint256(rocketNetworkSnapshots.latestValue(settingKey)); } /// @notice Returns the `reduced_bond` variable used in bond requirements calculation function getReducedBond() override external view returns (uint256) { return getSettingUint("reduced.bond"); } /// @notice Returns the `base_bond_array` mapping of number of validators to cumulative bond requirement function getBaseBondArray() override public pure returns (uint256[] memory) { uint256[] memory amounts = new uint256[](2); amounts[0] = 4 ether; amounts[1] = 8 ether; return amounts; } /// @notice Returns the amount of time that must be waited after unstaking RPL before it can be returned function getUnstakingPeriod() override external view returns (uint256) { return getSettingUint("node.unstaking.period"); } /// @notice Returns the amount of time that must be waited after staking RPL before it can be unstaked again function getWithdrawalCooldown() override external view returns (uint256) { return getSettingUint("node.withdrawal.cooldown"); } /// @notice Returns the amount of legacy staked RPL required by a node after unstaking as percentage of their borrowed ETH function getMinimumLegacyRPLStake() override external view returns (uint256) { return getSettingUint("node.minimum.legacy.staked.rpl"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsProposalsInterface.sol"; /// @notice Settings related to proposals in the protocol DAO contract RocketDAOProtocolSettingsProposals is RocketDAOProtocolSettings, RocketDAOProtocolSettingsProposalsInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "proposals") { version = 3; // Initialize settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings setSettingUint("proposal.vote.phase1.time", 1 weeks); // How long a proposal can be voted on in phase 1 setSettingUint("proposal.vote.phase2.time", 1 weeks); // How long a proposal can be voted on in phase 2 setSettingUint("proposal.vote.delay.time", 1 weeks); // How long before a proposal can be voted on after it is created setSettingUint("proposal.execute.time", 4 weeks); // How long a proposal can be executed after its voting period is finished setSettingUint("proposal.bond", 100 ether); // The amount of RPL a proposer has to put up as a bond for creating a new proposal setSettingUint("proposal.challenge.bond", 10 ether); // The amount of RPL a challenger has to put up as a bond for challenging a proposal setSettingUint("proposal.challenge.period", 30 minutes); // The amount of time a proposer has to respond to a challenge before a proposal is defeated setSettingUint("proposal.quorum", 0.15 ether); // The quorum required to pass a proposal (RPIP-64) setSettingUint("proposal.veto.quorum", 0.20 ether); // The quorum required to veto a proposal (RPIP-64) setSettingUint("proposal.max.block.age", 1024); // The maximum age of a block a proposal can be raised at // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @notice Update a setting, overrides inherited setting method with extra checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings bytes32 settingKey = keccak256(bytes(_settingPath)); if(settingKey == keccak256(bytes("proposal.vote.phase1.time"))) { // Must be at least 1 day (RPIP-33) require(_value >= 1 days, "Value must be at least 1 day"); } else if(settingKey == keccak256(bytes("proposal.vote.phase2.time"))) { // Must be at least 1 day (RPIP-33) require(_value >= 1 days, "Value must be at least 1 day"); } else if(settingKey == keccak256(bytes("proposal.vote.delay.time"))) { // Must be at least 1 week (RPIP-33) require(_value >= 1 weeks, "Value must be at least 1 week"); } else if(settingKey == keccak256(bytes("proposal.execute.time"))) { // Must be at least 1 week (RPIP-33) require(_value >= 1 weeks, "Value must be at least 1 week"); } else if(settingKey == keccak256(bytes("proposal.bond"))) { // Must be higher than 20 RPL(RPIP-33) require(_value > 20 ether, "Value must be higher than 20 RPL"); } else if(settingKey == keccak256(bytes("proposal.challenge.bond"))) { // Must be higher than 2 RPL(RPIP-33) require(_value > 2 ether, "Value must be higher than 2 RPL"); } else if(settingKey == keccak256(bytes("proposal.challenge.period"))) { // Must be at least 30 minutes (RPIP-33) require(_value >= 30 minutes, "Value must be at least 30 minutes"); } else if(settingKey == keccak256(bytes("proposal.quorum"))) { // Must be >= 15% & < 75% (RPIP-63) require(_value >= 0.15 ether && _value < 0.75 ether, "Value must be >= 15% & < 75%"); } else if(settingKey == keccak256(bytes("proposal.veto.quorum"))) { // Must be >= 20% & < 75% (RPIP-64) require(_value >= 0.20 ether && _value < 0.75 ether, "Value must be >= 20% & < 75%"); } else if(settingKey == keccak256(bytes("proposal.max.block.age"))) { // Must be > 128 blocks & < 7200 blocks (RPIP-33) require(_value > 128 && _value < 7200, "Value must be > 128 blocks & < 7200 blocks"); } // Update setting now setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice How long a proposal can be voted on in phase 1 function getVotePhase1Time() override external view returns (uint256) { return getSettingUint("proposal.vote.phase1.time"); } /// @notice How long a proposal can be voted on in phase 2 function getVotePhase2Time() override external view returns (uint256) { return getSettingUint("proposal.vote.phase2.time"); } /// @notice How long before a proposal can be voted on after it is created function getVoteDelayTime() override external view returns (uint256) { return getSettingUint("proposal.vote.delay.time"); } /// @notice How long a proposal can be executed after its voting period is finished function getExecuteTime() override external view returns (uint256) { return getSettingUint("proposal.execute.time"); } /// @notice The amount of RPL that is locked when raising a proposal function getProposalBond() override external view returns (uint256) { return getSettingUint("proposal.bond"); } /// @notice The amount of RPL that is locked when challenging a proposal function getChallengeBond() override external view returns (uint256) { return getSettingUint("proposal.challenge.bond"); } /// @notice How long (in seconds) a proposer has to respond to a challenge function getChallengePeriod() override external view returns (uint256) { return getSettingUint("proposal.challenge.period"); } /// @notice The quorum required for a proposal to be successful (as a fraction of 1e18) function getProposalQuorum() override external view returns (uint256) { return getSettingUint("proposal.quorum"); } /// @notice The quorum required for a proposal veto to be successful (as a fraction of 1e18) function getProposalVetoQuorum() override external view returns (uint256) { return getSettingUint("proposal.veto.quorum"); } /// @notice The maximum time in the past (in blocks) a proposal can be submitted for function getProposalMaxBlockAge() override external view returns (uint256) { return getSettingUint("proposal.max.block.age"); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsRewards.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; /// @notice Settings relating to RPL reward intervals contract RocketDAOProtocolSettingsRewards is RocketDAOProtocolSettings, RocketDAOProtocolSettingsRewardsInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "rewards") { version = 2; // Set some initial settings on first deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // RPL Claims settings setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimTrustedNode")), 0.2 ether); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimDAO")), 0.1 ether); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimNode")), 0.7 ether); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount.updated.time")), block.timestamp); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "periods")), 28); // The number of submission periods in which a claim period will span - 28 periods = 28 days by default // Deployment check setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); // Flag that this contract has been deployed, so default settings don't get reapplied on a contract upgrade } } /*** Settings ****************/ /// @notice Updates the percentages the trusted nodes use when calculating RPL reward trees. Percentages must add up to 100% /// @param _trustedNodePercent The percentage of rewards paid to the trusted node set (as a fraction of 1e18) /// @param _protocolPercent The percentage of rewards paid to the protocol dao (as a fraction of 1e18) /// @param _nodePercent The percentage of rewards paid to the node operators (as a fraction of 1e18) function setSettingRewardsClaimers(uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent) override external onlyDAOProtocolProposal { // Check total require(_trustedNodePercent + _protocolPercent + _nodePercent == 1 ether, "Total does not equal 100%"); // Update now setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimTrustedNode")), _trustedNodePercent); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimDAO")), _protocolPercent); setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimNode")), _nodePercent); // Set time last updated setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount.updated.time")), block.timestamp); } /// @notice Get the percentage of rewards paid to a contract by its internal name. Deprecated in favour of individual /// getRewardClaimers*Perc() methods. Retained for backwards compatibility. function getRewardsClaimerPerc(string memory _contractName) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", _contractName))); } /// @notice Get the percentages paid to each of the reward recipients on each internval function getRewardsClaimersPerc() override public view returns (uint256 trustedNodePerc, uint256 protocolPerc, uint256 nodePerc) { trustedNodePerc = getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimTrustedNode"))); protocolPerc = getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimDAO"))); nodePerc = getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimNode"))); } /// @notice Get the percentage of rewards paid to the trusted nodes function getRewardsClaimersTrustedNodePerc() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimTrustedNode"))); } /// @notice Get the percentage of rewards paid to the protocol dao function getRewardsClaimersProtocolPerc() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimDAO"))); } /// @notice Get the percentage of rewards paid to the node operators function getRewardsClaimersNodePerc() override public view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount", "rocketClaimNode"))); } /// @notice Get the time of when the claim percentages were last updated function getRewardsClaimersTimeUpdated() override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "group.amount.updated.time"))); } /// @notice The number of submission periods after which claims can be made function getRewardsClaimIntervalPeriods() override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "periods"))); } /// @notice The interval time for reward periods function getRewardsClaimIntervalTime() override external view returns (uint256) { RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); return getUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "periods"))) * rocketDAOProtocolSettingsNetwork.getSubmitBalancesFrequency(); } } ================================================ FILE: contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsSecurity.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../../../interface/RocketStorageInterface.sol"; import {RocketDAOProtocolSettings} from "./RocketDAOProtocolSettings.sol"; import {RocketDAOProtocolSettingsSecurityInterface} from "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; /// @notice Protocol parameters relating to the security council contract RocketDAOProtocolSettingsSecurity is RocketDAOProtocolSettings, RocketDAOProtocolSettingsSecurityInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "security") { version = 2; // Initialise settings on deployment if (!rocketStorage.getDeployedStatus()) { // Set defaults _setSettingUint("members.quorum", 0.51 ether); // Member quorum threshold that must be met for proposals to pass (51%) _setSettingUint("members.leave.time", 4 weeks); // How long a member must give notice for before manually leaving the security council _setSettingUint("proposal.vote.time", 2 weeks); // How long a proposal can be voted on _setSettingUint("proposal.execute.time", 4 weeks); // How long a proposal can be executed after its voting period is finished _setSettingUint("proposal.action.time", 4 weeks); // Certain proposals require a secondary action to be run after the proposal is successful (joining, leaving etc). This is how long until that action expires _setSettingUint("upgradeveto.quorum", 0.33 ether); // RPIP-60: Member quorum threshold to veto a protocol upgrade (33%) _setSettingUint("upgrade.delay", 7 days); // RPIP-60: Amount of time after an upgrade proposal passes that the security has to veto it // Default permissions for security council setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "deposit", "deposit.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "deposit", "deposit.assign.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "minipool", "minipool.submit.withdrawable.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "minipool", "minipool.bond.reduction.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "network", "network.submit.balances.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "network", "network.submit.prices.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "network", "network.submit.rewards.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "node", "node.registration.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "node", "node.smoothing.pool.registration.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "node", "node.deposit.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "node", "node.vacant.minipools.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "auction", "auction.lot.create.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "auction", "auction.lot.bidding.enabled")), true); setBool(keccak256(abi.encodePacked("dao.security.allowed.setting", "network", "network.node.commission.share.security.council.adder")), true); // Set deploy flag setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); } } /// @dev Overrides inherited setting method with extra sanity checks for this contract function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal { // Some safety guards for certain settings if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { bytes32 settingKey = keccak256(abi.encodePacked(_settingPath)); if(settingKey == keccak256(bytes("members.quorum"))) { require(_value >= 0.51 ether && _value <= 0.75 ether, "Quorum setting must be >= 51% & <= 75%"); } else if(settingKey == keccak256(bytes("members.leave.time"))) { require(_value < 14 days, "Value must be < 14 days"); } else if(settingKey == keccak256(bytes("proposal.vote.time"))) { require(_value >= 1 days, "Value must be >= 1 day"); } else if(settingKey == keccak256(bytes("proposal.execute.time"))) { require(_value >= 1 days, "Value must be >= 1 day"); } else if(settingKey == keccak256(bytes("proposal.action.time"))) { require(_value >= 1 days, "Value must be >= 1 day"); } else if(settingKey == keccak256(bytes("upgradeveto.quorum"))) { require(_value >= 0.33 ether && _value <= 1 ether, "Quorum setting must be >= 33% & <= 100%"); } else if(settingKey == keccak256(bytes("upgrade.delay"))) { require(_value >= 1 days && _value <= 30 days, "Value must be >= 1 day & <= 30 days"); } } // Update setting now _setSettingUint(_settingPath, _value); } /// @dev Sets a namespaced uint value skipping any guardrails function _setSettingUint(string memory _settingPath, uint256 _value) internal { setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value); } /// @notice The member proposal quorum threshold for this DAO function getQuorum() override external view returns (uint256) { return getSettingUint("members.quorum"); } /// @notice How long a member must give notice before leaving function getLeaveTime() override external view returns (uint256) { return getSettingUint("members.leave.time"); } /// @notice How long a proposal can be voted on function getVoteTime() override external view returns (uint256) { return getSettingUint("proposal.vote.time"); } /// @notice How long a proposal can be executed after its voting period is finished function getExecuteTime() override external view returns (uint256) { return getSettingUint("proposal.execute.time"); } /// @notice Certain proposals require a secondary action to be run after the proposal is successful (joining, leaving etc). This is how long until that action expires function getActionTime() override external view returns (uint256) { return getSettingUint("proposal.action.time"); } /// @notice The quorum required by the security council to veto an upgrade function getUpgradeVetoQuorum() override external view returns (uint256) { return getSettingUint("upgradeveto.quorum"); } /// @notice The amount of time that must be waited after an upgrade before executing function getUpgradeDelay() override external view returns (uint256) { return getSettingUint("upgrade.delay"); } } ================================================ FILE: contracts/contract/dao/security/RocketDAOSecurity.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../RocketBase.sol"; import "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityProposalsInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; import "../../../interface/util/AddressSetStorageInterface.sol"; /// @notice The Rocket Pool Security Council DAO contract RocketDAOSecurity is RocketBase, RocketDAOSecurityInterface { // The namespace for any data stored in the network DAO (do not change) string constant internal daoNameSpace = "dao.security."; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @notice Return the amount of member votes need for a proposal to pass (as a fraction of 1e18) function getMemberQuorumVotesRequired() override external view returns (uint256) { // Load contracts RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); // Calculate and return votes required return getMemberCount() * rocketDAOProtocolSettingsSecurity.getQuorum(); } /*** Members ******************/ /// @notice Returns whether a given address is a member /// @param _memberAddress Address of the member to query /// @return True if the node addressed passed is a member of the trusted node DAO function getMemberIsValid(address _memberAddress) override external view returns (bool) { return getBool(keccak256(abi.encodePacked(daoNameSpace, "member", _memberAddress))); } /// @notice Returns the address of a node in the member set /// @param _index The index into the member set /// @return Address of the member at the given index function getMemberAt(uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _index); } /// @notice Returns the total number of members in the set /// @return The number of members function getMemberCount() override public view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked(daoNameSpace, "member.index"))); } /// @notice Get the ID of a member /// @param _memberAddress The address of the member to query /// @return The ID of the relevant member function getMemberID(address _memberAddress) override external view returns (string memory) { return getString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _memberAddress))); } /// @notice Get the block the member joined at /// @param _memberAddress The address of the member to query /// @return The timestamp at which the relevant member joined function getMemberJoinedTime(address _memberAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _memberAddress))); } /// @notice Get data that was recorded about a proposal that was executed /// @param _proposalType Can be one of the following: "invited", "leave" /// @param _memberAddress The address of the member to query /// @return The timestamp that the relevant proposal happened for the relevant member function getMemberProposalExecutedTime(string memory _proposalType, address _memberAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", _proposalType, _memberAddress))); } } ================================================ FILE: contracts/contract/dao/security/RocketDAOSecurityActions.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../RocketBase.sol"; import "../../../interface/RocketVaultInterface.sol"; import "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityActionsInterface.sol"; import "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import "../../../interface/util/IERC20Burnable.sol"; import "../../../interface/util/AddressSetStorageInterface.sol"; /// @notice Executes proposals which affect security council members contract RocketDAOSecurityActions is RocketBase, RocketDAOSecurityActionsInterface { // The namespace for any data stored in the network DAO (do not change) string constant internal daoNameSpace = "dao.security."; // Events event ActionJoined(address indexed nodeAddress, uint256 time); event ActionLeave(address indexed nodeAddress, uint256 time); event ActionRequestLeave(address indexed nodeAddress, uint256 time); event ActionKick(address indexed nodeAddress, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /*** Action Methods ************************/ /// @notice Removes a member from the security council /// @param _memberAddress The address of the member to kick function actionKick(address _memberAddress) override public onlyLatestContract("rocketDAOSecurityProposals", msg.sender) { // Remove the member now _memberRemove(_memberAddress); // Log it emit ActionKick(_memberAddress, block.timestamp); } /// @notice Removes multiple members from the security council /// @param _memberAddresses An array of addresses of the member to kick function actionKickMulti(address[] calldata _memberAddresses) override external onlyLatestContract("rocketDAOSecurityProposals", msg.sender) { // Remove the members for (uint256 i = 0; i < _memberAddresses.length; ++i) { actionKick(_memberAddresses[i]); } } /// @notice An invited member can execute this function to join the security council function actionJoin() override external onlyLatestContract("rocketDAOSecurityActions", address(this)) { // Add the member _memberJoin(msg.sender); // Log it emit ActionJoined(msg.sender, block.timestamp); } /// @notice A member who wishes to leave the security council can call this method to initiate the process function actionRequestLeave() override external onlyLatestContract("rocketDAOSecurityActions", address(this)) { // Load contracts RocketDAOSecurityInterface rocketDAOSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); // Check they are currently a member require(rocketDAOSecurity.getMemberIsValid(msg.sender), "Not a current member"); // Update the leave time to include the required notice period set by the protocol DAO setUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "leave", msg.sender)), block.timestamp + rocketDAOProtocolSettingsSecurity.getLeaveTime()); // Log it emit ActionRequestLeave(msg.sender, block.timestamp); } /// @notice A member who has asked to leave and waited the required time can call this method to formally leave the security council function actionLeave() override external onlyLatestContract("rocketDAOSecurityActions", address(this)) { // Load contracts RocketDAOSecurityInterface rocketDAOSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); // Check they are currently a member require(rocketDAOSecurity.getMemberIsValid(msg.sender), "Not a current member"); // Get the time that they were approved to leave at uint256 leaveAcceptedTime = rocketDAOSecurity.getMemberProposalExecutedTime("leave", msg.sender); // Has the member waiting long enough? require(leaveAcceptedTime < block.timestamp, "Member has not waited required time to leave"); // Has the leave request expired? require(leaveAcceptedTime + rocketDAOProtocolSettingsSecurity.getActionTime() > block.timestamp, "This member has not been approved to leave or request has expired, please apply to leave again"); // Remove them now _memberRemove(msg.sender); // Log it emit ActionLeave(msg.sender, block.timestamp); } /*** Internal Methods ************************/ /// @dev Removes a member from the security council function _memberRemove(address _nodeAddress) private { // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Remove their membership now deleteBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress))); deleteAddress(keccak256(abi.encodePacked(daoNameSpace, "member.address", _nodeAddress))); deleteString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress))); // Clean up the invited/leave proposals deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "invited", _nodeAddress))); deleteUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "leave", _nodeAddress))); // Remove from member index now addressSetStorage.removeItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _nodeAddress); } /// @dev A member official joins the security council, if successful they are added as a member function _memberJoin(address _nodeAddress) private { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Load contracts RocketDAOSecurityInterface rocketDAOSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); // The time that the member was successfully invited to join the DAO uint256 memberInvitedTime = rocketDAOSecurity.getMemberProposalExecutedTime("invited", _nodeAddress); // Have they been invited? require(memberInvitedTime > 0, "This address has not been invited to join"); // Has their invite expired? require(memberInvitedTime + rocketDAOProtocolSettingsSecurity.getActionTime() > block.timestamp, "This node's invitation to join has expired, please apply again"); // Check current node status require(rocketDAOSecurity.getMemberIsValid(_nodeAddress) != true, "This node is already part of the security council"); // Mark them as a valid security council member setBool(keccak256(abi.encodePacked(daoNameSpace, "member", _nodeAddress)), true); // Record the block number they joined at setUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _nodeAddress)), block.timestamp); // Add to member index now addressSetStorage.addItem(keccak256(abi.encodePacked(daoNameSpace, "member.index")), _nodeAddress); } } ================================================ FILE: contracts/contract/dao/security/RocketDAOSecurityProposals.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketBase} from "../../RocketBase.sol"; import {RocketStorageInterface} from "../../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../../interface/RocketVaultInterface.sol"; import {RocketDAOProposalInterface} from "../../../interface/dao/RocketDAOProposalInterface.sol"; import {RocketDAOProtocolInterface} from "../../../interface/dao/protocol/RocketDAOProtocolInterface.sol"; import {RocketDAOProtocolSettingsSecurityInterface} from "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketDAOSecurityActionsInterface} from "../../../interface/dao/security/RocketDAOSecurityActionsInterface.sol"; import {RocketDAOSecurityInterface} from "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import {RocketDAOSecurityProposalsInterface} from "../../../interface/dao/security/RocketDAOSecurityProposalsInterface.sol"; import {RocketNetworkRevenuesInterface} from "../../../interface/network/RocketNetworkRevenuesInterface.sol"; import {IERC20Burnable} from "../../../interface/util/IERC20Burnable.sol"; /// @notice Proposal contract for the security council contract RocketDAOSecurityProposals is RocketBase, RocketDAOSecurityProposalsInterface { // The namespace for any data stored in the trusted node DAO (do not change) string constant internal daoNameSpace = "dao.security."; // The namespace of the DAO that setting changes get applied to (protocol DAO) string constant internal protocolDaoSettingNamespace = "dao.protocol.setting."; /// @dev Only allow certain contracts to execute methods modifier onlyExecutingContracts() { // Methods are executed by people executing passed proposals in rocketDAOProposal require(msg.sender == getContractAddress("rocketDAOProposal"), "Sender is not permitted to access executing methods"); _; } /// @dev Only allow security councils to vote modifier onlySecurityMember() { require(getBool(keccak256(abi.encodePacked(daoNameSpace, "member", msg.sender))), "Sender is not a security council member"); _; } /// @dev Reverts if the provided setting is not within the accepted set of settings modifier onlyValidSetting(string memory _settingNameSpace, string memory _settingPath) { if (!getBool(keccak256(abi.encodePacked("dao.security.allowed.setting", _settingNameSpace, _settingPath)))) { revert("Setting is not modifiable by security council"); } _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Creates a new proposal for this DAO /// @param _proposalMessage A short message explaining what this proposal does /// @param _payload An ABI encoded payload which is executed on the proposal contract upon execution of this proposal function propose(string memory _proposalMessage, bytes memory _payload) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityProposals", address(this)) returns (uint256) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); // Create the proposal return daoProposal.add(msg.sender, "rocketDAOSecurityProposals", _proposalMessage, block.timestamp + 1, rocketDAOProtocolSettingsSecurity.getVoteTime(), rocketDAOProtocolSettingsSecurity.getExecuteTime(), daoSecurity.getMemberQuorumVotesRequired(), _payload); } /// @notice Vote on a proposal /// @param _proposalID The ID of the proposal to vote on /// @param _support Whether the caller votes in favour or against the proposal function vote(uint256 _proposalID, bool _support) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); // Did they join after this proposal was created? If so, they can't vote or it'll throw off the set proposalVotesRequired require(daoSecurity.getMemberJoinedTime(msg.sender) < daoProposal.getCreated(_proposalID), "Member cannot vote on proposal created before they became a member"); // Vote now, one vote per trusted node member daoProposal.vote(msg.sender, 1 ether, _proposalID, _support); } /// @notice Cancel a proposal /// @param _proposalID The ID of the proposal to cancel function cancel(uint256 _proposalID) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Cancel now, will succeed if it is the original proposer daoProposal.cancel(msg.sender, _proposalID); } /// @notice Execute a successful proposal /// @param _proposalID The ID of the proposal to execute function execute(uint256 _proposalID) override external onlyLatestContract("rocketDAOSecurityProposals", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Execute now daoProposal.execute(_proposalID); } /*** Proposal - Settings **********************/ /// @notice Change one of the current uint256 settings of the protocol DAO /// @param _settingNameSpace The namespace of the setting to change /// @param _settingPath The setting path to change /// @param _value The new value for the setting function proposalSettingUint(string memory _settingNameSpace, string memory _settingPath, uint256 _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) { bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace)); setUint(keccak256(abi.encodePacked(namespace, _settingPath)), _value); // Security council adder requires additional processing if (keccak256(bytes(_settingNameSpace)) == keccak256(bytes("network"))) { if (keccak256(bytes(_settingPath)) == keccak256(bytes("network.node.commission.share.security.council.adder"))) { RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); // Check guardrails uint256 maxAdderValue = rocketDAOProtocolSettingsNetwork.getMaxNodeShareSecurityCouncilAdder(); require(_value <= maxAdderValue, "Value must be <= max value"); uint256 maxVoterValue = rocketDAOProtocolSettingsNetwork.getVoterShare(); require(_value <= maxVoterValue, "Value must be <= voter share"); // Notify RocketNetworkRevenue of the changes to voter and node share RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); rocketNetworkRevenues.setVoterShare(rocketDAOProtocolSettingsNetwork.getEffectiveVoterShare()); rocketNetworkRevenues.setNodeShare(rocketDAOProtocolSettingsNetwork.getEffectiveNodeShare()); } } } /// @notice Change one of the current bool settings of the protocol DAO /// @param _settingNameSpace The namespace of the setting to change /// @param _settingPath The setting path to change /// @param _value The new value for the setting function proposalSettingBool(string memory _settingNameSpace, string memory _settingPath, bool _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) { bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace)); setBool(keccak256(abi.encodePacked(namespace, _settingPath)), _value); } /// @notice Change one of the current address settings of the protocol DAO /// @param _settingNameSpace The namespace of the setting to change /// @param _settingPath The setting path to change /// @param _value The new value for the setting function proposalSettingAddress(string memory _settingNameSpace, string memory _settingPath, address _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) { bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace)); setAddress(keccak256(abi.encodePacked(namespace, _settingPath)), _value); } /*** Proposal - Members **********************/ /// @dev Called by rocketDAOProtocolProposals to execute an invite in this namespace /// @param _id A unique identifier for the new member /// @param _memberAddress The address of the new member function proposalInvite(string calldata _id, address _memberAddress) override public onlyLatestContract("rocketDAOProtocolProposals", msg.sender) { // Their proposal executed, record the block setUint(keccak256(abi.encodePacked(daoNameSpace, "member.executed.time", "invited", _memberAddress)), block.timestamp); // Ok all good, lets get their invitation and member data setup // They are initially only invited to join, so their membership isn't set as true until they accept it in RocketDAONodeTrustedActions _memberInit(_id, _memberAddress); } /// @dev Called by rocketDAOProtocolProposals to execute a kick in this namespace /// @param _memberAddress The address of the member to kick function proposalKick(address _memberAddress) override public onlyLatestContract("rocketDAOProtocolProposals", msg.sender) { // Load contracts RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOSecurityActionsInterface daoActionsContract = RocketDAOSecurityActionsInterface(getContractAddress("rocketDAOSecurityActions")); // Check valid member require(daoSecurity.getMemberIsValid(_memberAddress), "This node is not part of the security council"); // Kick them now daoActionsContract.actionKick(_memberAddress); } /// @dev Called by rocketDAOProtocolProposals to execute a kick of multiple members in this namespace /// @param _memberAddresses An array of addresses of the members to kick function proposalKickMulti(address[] calldata _memberAddresses) override public onlyLatestContract("rocketDAOProtocolProposals", msg.sender) { // Load contracts RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOSecurityActionsInterface daoActionsContract = RocketDAOSecurityActionsInterface(getContractAddress("rocketDAOSecurityActions")); // Check valid members for (uint256 i = 0; i < _memberAddresses.length; ++i) { require(daoSecurity.getMemberIsValid(_memberAddresses[i]), "This node is not part of the security council"); } // Kick them now daoActionsContract.actionKickMulti(_memberAddresses); } /// @dev Called by rocketDAOProtocolProposals to execute an member replacement in this namespace /// @param _existingMemberAddress The address of the member to kick /// @param _newMemberId A unique identifier for the new member /// @param _newMemberAddress The address of the member to invite function proposalReplace(address _existingMemberAddress, string calldata _newMemberId, address _newMemberAddress) override external onlyLatestContract("rocketDAOProtocolProposals", msg.sender) { // Load contracts RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); // Check valid member require(daoSecurity.getMemberIsValid(_existingMemberAddress), "This node is not part of the security council"); require(_existingMemberAddress != _newMemberAddress, "New member address must not be the same"); // Kick and invite proposalKick(_existingMemberAddress); proposalInvite(_newMemberId, _newMemberAddress); } /*** Methods - Internal ***************/ /// @dev Add a new potential members data, they must accept the invite to become an actual member /// @param _id A unique ID for the new member /// @param _memberAddress The address of the new member function _memberInit(string memory _id, address _memberAddress) private { // Load contracts RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); // Check current node status require(!daoSecurity.getMemberIsValid(_memberAddress), "This node is already part of the security council"); // Verify the ID is min 3 chars require(bytes(_id).length >= 3, "The ID for this new member must be at least 3 characters"); // Member initial data, not official until the bool is flagged as true setBool(keccak256(abi.encodePacked(daoNameSpace, "member", _memberAddress)), false); setAddress(keccak256(abi.encodePacked(daoNameSpace, "member.address", _memberAddress)), _memberAddress); setString(keccak256(abi.encodePacked(daoNameSpace, "member.id", _memberAddress)), _id); setUint(keccak256(abi.encodePacked(daoNameSpace, "member.joined.time", _memberAddress)), 0); } } ================================================ FILE: contracts/contract/dao/security/RocketDAOSecurityUpgrade.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketBase} from "../../RocketBase.sol"; import {RocketStorageInterface} from "../../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../../interface/RocketVaultInterface.sol"; import {RocketDAOProposalInterface} from "../../../interface/dao/RocketDAOProposalInterface.sol"; import {RocketDAOProtocolInterface} from "../../../interface/dao/protocol/RocketDAOProtocolInterface.sol"; import {RocketDAOProtocolSettingsSecurityInterface} from "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketDAOSecurityActionsInterface} from "../../../interface/dao/security/RocketDAOSecurityActionsInterface.sol"; import {RocketDAOSecurityInterface} from "../../../interface/dao/security/RocketDAOSecurityInterface.sol"; import {RocketDAOSecurityUpgradeInterface} from "../../../interface/dao/security/RocketDAOSecurityUpgradeInterface.sol"; import {RocketNetworkRevenuesInterface} from "../../../interface/network/RocketNetworkRevenuesInterface.sol"; import {IERC20Burnable} from "../../../interface/util/IERC20Burnable.sol"; import {RocketDAONodeTrustedUpgradeInterface} from "../../../interface/dao/node/RocketDAONodeTrustedUpgradeInterface.sol"; /// @notice Proposal contract for the security council upgrade veto powers contract RocketDAOSecurityUpgrade is RocketBase, RocketDAOSecurityUpgradeInterface { // The namespace for any data stored in the security DAO (do not change) string constant internal daoNameSpace = "dao.security."; /// @dev Only allow certain contracts to execute methods modifier onlyExecutingContracts() { // Methods are executed by people executing passed proposals in rocketDAOProposal require(msg.sender == getContractAddress("rocketDAOProposal"), "Sender is not permitted to access executing methods"); _; } /// @dev Only allow security councils to vote modifier onlySecurityMember() { require(getBool(keccak256(abi.encodePacked(daoNameSpace, "member", msg.sender))), "Sender is not a security council member"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @notice Creates a new upgrade veto proposal for this DAO /// @param _proposalMessage A short message explaining what this proposal does /// @param _upgradeProposalID ID of the upgrade proposal to propose a veto for function proposeVeto(string memory _proposalMessage, uint256 _upgradeProposalID) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityUpgrade", address(this)) returns (uint256) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); RocketDAOProtocolSettingsSecurityInterface rocketDAOProtocolSettingsSecurity = RocketDAOProtocolSettingsSecurityInterface(getContractAddress("rocketDAOProtocolSettingsSecurity")); RocketDAONodeTrustedUpgradeInterface rocketDAONodeTrustedUpgrade = RocketDAONodeTrustedUpgradeInterface(getContractAddress("rocketDAONodeTrustedUpgrade")); // Check proposal is pending require(rocketDAONodeTrustedUpgrade.getState(_upgradeProposalID) == RocketDAONodeTrustedUpgradeInterface.UpgradeProposalState.Pending); // Calculate veto quorum required uint256 vetoQuorum = daoSecurity.getMemberCount() * rocketDAOProtocolSettingsSecurity.getUpgradeVetoQuorum(); // Construct veto payload bytes memory payload = abi.encodeWithSelector(this.proposalVeto.selector, _upgradeProposalID); // Create the proposal return daoProposal.add(msg.sender, "rocketDAOSecurityUpgrade", _proposalMessage, block.timestamp + 1, rocketDAOProtocolSettingsSecurity.getVoteTime(), rocketDAOProtocolSettingsSecurity.getExecuteTime(), vetoQuorum, payload); } /// @notice Vote on a proposal /// @param _proposalID The ID of the proposal to vote on /// @param _support Whether the caller votes in favour or against the proposal function vote(uint256 _proposalID, bool _support) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityUpgrade", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); RocketDAOSecurityInterface daoSecurity = RocketDAOSecurityInterface(getContractAddress("rocketDAOSecurity")); // Did they join after this proposal was created? If so, they can't vote or it'll throw off the set proposalVotesRequired require(daoSecurity.getMemberJoinedTime(msg.sender) < daoProposal.getCreated(_proposalID), "Member cannot vote on proposal created before they became a member"); // Vote now, one vote per security council member daoProposal.vote(msg.sender, 1 ether, _proposalID, _support); } /// @notice Cancel a proposal /// @param _proposalID The ID of the proposal to cancel function cancel(uint256 _proposalID) override external onlySecurityMember() onlyLatestContract("rocketDAOSecurityUpgrade", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Cancel now, will succeed if it is the original proposer daoProposal.cancel(msg.sender, _proposalID); } /// @notice Execute a successful proposal /// @param _proposalID The ID of the proposal to execute function execute(uint256 _proposalID) override external onlyLatestContract("rocketDAOSecurityUpgrade", address(this)) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress("rocketDAOProposal")); // Execute now daoProposal.execute(_proposalID); } /// @notice Veto a protocol upgrade /// @param _upgradeProposalID The ID of the upgrade to veto function proposalVeto(uint256 _upgradeProposalID) override public onlyExecutingContracts { RocketDAONodeTrustedUpgradeInterface rocketDAONodeTrustedUpgrade = RocketDAONodeTrustedUpgradeInterface(getContractAddress("rocketDAONodeTrustedUpgrade")); rocketDAONodeTrustedUpgrade.veto(_upgradeProposalID); } } ================================================ FILE: contracts/contract/deposit/RocketDepositPool.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketNetworkBalancesInterface} from "../../interface/network/RocketNetworkBalancesInterface.sol"; import {AddressQueueStorageInterface} from "../../interface/util/AddressQueueStorageInterface.sol"; import {LinkedListStorageInterface} from "../../interface/util/LinkedListStorageInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketDAOProtocolSettingsDepositInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsDepositInterface.sol"; import {RocketDAOProtocolSettingsMinipoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketDepositPoolInterface} from "../../interface/deposit/RocketDepositPoolInterface.sol"; import {RocketMegapoolDelegateInterface} from "../../interface/megapool/RocketMegapoolDelegateInterface.sol"; import {RocketMegapoolInterface} from "../../interface/megapool/RocketMegapoolInterface.sol"; import {RocketMinipoolInterface} from "../../interface/minipool/RocketMinipoolInterface.sol"; import {RocketMinipoolQueueInterface} from "../../interface/minipool/RocketMinipoolQueueInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketTokenRETHInterface} from "../../interface/token/RocketTokenRETHInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; import {RocketMegapoolFactoryInterface} from "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; /// @notice Accepts user deposits and mints rETH; handles assignment of deposited ETH to megapools contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaultWithdrawerInterface { // Constants uint256 internal constant milliToWei = 10 ** 15; bytes32 internal constant queueKeyMinipoolVariable = keccak256("minipools.available.variable"); bytes32 internal constant expressQueueNamespace = keccak256("deposit.queue.express"); bytes32 internal constant standardQueueNamespace = keccak256("deposit.queue.standard"); bytes32 internal constant queueMovedKey = keccak256("megapool.queue.moved"); bytes32 internal constant nodeBalanceKey = "deposit.pool.node.balance"; // Note: this is not hashed due to bug in earlier contract bytes32 internal constant requestedTotalKey = keccak256("deposit.pool.requested.total"); bytes32 internal constant queueIndexKey = keccak256("megapool.queue.index"); // Immutables RocketVaultInterface immutable internal rocketVault; RocketTokenRETHInterface immutable internal rocketTokenRETH; // Events event DepositReceived(address indexed from, uint256 amount, uint256 time); event DepositRecycled(address indexed from, uint256 amount, uint256 time); event DepositAssigned(address indexed minipool, uint256 amount, uint256 time); event ExcessWithdrawn(address indexed to, uint256 amount, uint256 time); event FundsRequested(address indexed receiver, uint256 validatorId, uint256 amount, bool expressQueue, uint256 time); event FundsAssigned(address indexed receiver, uint256 amount, uint256 time); event QueueExited(address indexed nodeAddress, uint256 time); event CreditWithdrawn(address indexed nodeAddress, uint256 amount, uint256 time); // Structs struct MinipoolAssignment { address minipoolAddress; uint256 etherAssigned; } // Modifiers modifier onlyThisLatestContract() { // Compiler can optimise out this keccak at compile time require(address(this) == getAddress(keccak256("contract.addressrocketDepositPool")), "Invalid or outdated contract"); _; } constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 4; // Pre-retrieve non-upgradable contract addresses to save gas rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketTokenRETH = RocketTokenRETHInterface(getContractAddress("rocketTokenRETH")); } /// @notice Returns the current deposit pool balance function getBalance() override public view returns (uint256) { return rocketVault.balanceOf("rocketDepositPool"); } /// @notice Returns the amount of ETH contributed to the deposit pool by node operators waiting in the queue function getNodeBalance() override public view returns (uint256) { return getUint(nodeBalanceKey); } /// @notice Returns the user owned portion of the deposit pool /// @dev Negative indicates more ETH has been "lent" to the deposit pool by node operators in the queue /// than is available from user deposits function getUserBalance() override public view returns (int256) { return int256(getBalance()) - int256(getNodeBalance()); } /// @notice Returns the credit balance for a given node operator /// @param _nodeAddress Address of the node operator to query function getNodeCreditBalance(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress))); } /// @notice Excess deposit pool balance (in excess of validator queue) function getExcessBalance() override public view returns (uint256) { // Get minipool queue capacity RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); uint256 capacity = rocketMinipoolQueue.getEffectiveCapacity(); capacity += getUint(requestedTotalKey); uint256 balance = getBalance(); // Calculate and return if (capacity >= balance) { return 0; } else { return balance - capacity; } } /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyThisLatestContract onlyLatestContract("rocketVault", msg.sender) {} /// @notice Deposits ETH into Rocket Pool and mints the corresponding amount of rETH to the caller function deposit() override external payable onlyThisLatestContract { // Check deposit settings RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled"); require(msg.value >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(), "The deposited amount is less than the minimum deposit size"); /* Check if deposit exceeds limit based on current deposit size and minipool queue capacity. The deposit pool can, at most, accept a deposit that, after assignments, matches ETH to every minipool in the queue and leaves the deposit pool with maximumDepositPoolSize ETH. capacityNeeded = depositPoolBalance + msg.value maxCapacity = maximumDepositPoolSize + queueEffectiveCapacity assert(capacityNeeded <= maxCapacity) */ uint256 capacityNeeded; unchecked { // Infeasible overflow capacityNeeded = getBalance() + msg.value; } uint256 maxDepositPoolSize = rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(); if (capacityNeeded > maxDepositPoolSize) { // Doing a conditional require() instead of a single one optimises for the common // case where capacityNeeded fits in the deposit pool without looking at the queue if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); uint256 capacity = rocketMinipoolQueue.getEffectiveCapacity(); capacity += getUint(requestedTotalKey); require(capacityNeeded <= maxDepositPoolSize + capacity, "The deposit pool size after depositing exceeds the maximum size"); } else { revert("The deposit pool size after depositing exceeds the maximum size"); } } // Calculate deposit fee unchecked { // depositFee < msg.value uint256 depositFee = msg.value * rocketDAOProtocolSettingsDeposit.getDepositFee() / calcBase; uint256 depositNet = msg.value - depositFee; // Mint rETH to user account rocketTokenRETH.mint(depositNet, msg.sender); } // Emit deposit received event emit DepositReceived(msg.sender, msg.value, block.timestamp); // Process deposit processDeposit(); } /// @notice Returns the maximum amount that can be accepted into the deposit pool at this time in wei function getMaximumDepositAmount() override external view returns (uint256) { RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); // If deposits are disabled max deposit is 0 if (!rocketDAOProtocolSettingsDeposit.getDepositEnabled()) { return 0; } uint256 depositPoolBalance = getBalance(); uint256 maxCapacity = rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(); // When assignments are enabled, we can accept the max amount plus whatever space is available in the minipool queue if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); maxCapacity += rocketMinipoolQueue.getEffectiveCapacity(); maxCapacity += getUint(requestedTotalKey); } // Check we aren't already over if (depositPoolBalance >= maxCapacity) { return 0; } return maxCapacity - depositPoolBalance; } /// @notice Accepts ETH deposit from the node deposit contract (does not mint rETH) /// @param _bondAmount The total node deposit amount including any credit balance used function nodeDeposit(uint256 _bondAmount) override external payable onlyThisLatestContract onlyLatestContract("rocketNodeDeposit", msg.sender) { // Deposit ETH into the vault if (msg.value > 0) { rocketVault.depositEther{value: msg.value}(); } // Increase recorded node balance addUint(nodeBalanceKey, _bondAmount); } /// @notice Recycle a deposit from a dissolved validator function recycleDissolvedDeposit() override external payable onlyThisLatestContract onlyRegisteredMinipoolOrMegapool(msg.sender) { _recycleValue(); } /// @notice Recycle excess ETH from the rETH token contract function recycleExcessCollateral() override external payable onlyThisLatestContract onlyLatestContract("rocketTokenRETH", msg.sender) { _recycleValue(); } /// @notice Recycle a liquidated RPL stake from a slashed minipool function recycleLiquidatedStake() override external payable onlyThisLatestContract onlyLatestContract("rocketAuctionManager", msg.sender) { _recycleValue(); } /// @dev Recycles msg.value into the deposit pool function _recycleValue() internal { // Recycle ETH emit DepositRecycled(msg.sender, msg.value, block.timestamp); processDeposit(); } /// @dev Deposits incoming funds into rETH buffer and excess into vault then performs assignment function processDeposit() internal { // Direct deposit ETH to rETH until target collateral is reached uint256 toReth = _getRethCollateralShortfall(); if (toReth > msg.value) { toReth = msg.value; } uint256 toVault = msg.value - toReth; if (toReth > 0) { rocketTokenRETH.depositExcess{value: toReth}(); } if (toVault > 0) { rocketVault.depositEther{value: toVault}(); } // Assign deposits if enabled _assignByDeposit(); } /// @dev Returns the shortfall in ETH from the target collateral rate of rETH function _getRethCollateralShortfall() internal returns (uint256) { // Load contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); RocketNetworkBalancesInterface rocketNetworkBalances = RocketNetworkBalancesInterface(getContractAddress("rocketNetworkBalances")); // Calculate target collateral uint256 targetCollateralRate = rocketDAOProtocolSettingsNetwork.getTargetRethCollateralRate(); uint256 rocketTokenRETHBalance = address(rocketTokenRETH).balance; uint256 totalCollateral = rocketNetworkBalances.getTotalETHBalance(); uint256 targetCollateral = totalCollateral * targetCollateralRate / calcBase; // Calculate shortfall if (targetCollateral > rocketTokenRETHBalance) { return targetCollateral - rocketTokenRETHBalance; } return 0; } /// @notice If deposit assignments are enabled, assigns up to specified number of minipools/megapools /// @param _max Maximum number of minipools/megapools to assign function maybeAssignDeposits(uint256 _max) override external onlyThisLatestContract { require(_max > 0, "Must assign at least 1"); RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); if (!rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { return; } _assignDeposits(_max, rocketDAOProtocolSettingsDeposit); } /// @notice Assigns up to specified number of minipools or megapools, reverts if assignments are disabled /// @param _max Maximum number of minipools/megapools to assign function assignDeposits(uint256 _max) override external onlyThisLatestContract { require(_max > 0, "Must assign at least 1"); RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); require(rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(), "Deposit assignments are disabled"); _assignDeposits(_max, rocketDAOProtocolSettingsDeposit); } /// @dev Assigns up to specified number of minipools or megapools /// @param _max Maximum number of minipools/megapools to assign function _assignDeposits(uint256 _max, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Get contracts AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); // Process minipool queue first uint256 minipoolQueueLength = addressQueueStorage.getLength(queueKeyMinipoolVariable); if (minipoolQueueLength > 0) { if (minipoolQueueLength >= _max) { _assignMinipools(_max, _rocketDAOProtocolSettingsDeposit); return; } else { unchecked { // _max > minipoolQueueLength _max -= minipoolQueueLength; } _assignMinipools(minipoolQueueLength, _rocketDAOProtocolSettingsDeposit); } } // Assign remainder to megapools if (_max > 0) { _assignMegapools(_max, _rocketDAOProtocolSettingsDeposit); } } /// @dev Assigns to minipools/megapools based on `msg.value`, does nothing if assignments are disabled function _assignByDeposit() internal { // Get contracts RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); // Check if assigning deposits is enabled if (!rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { return; } // Continue processing legacy minipool queue until empty AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); if (addressQueueStorage.getLength(queueKeyMinipoolVariable) > 0) { RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); _assignMinipoolsByDeposit(rocketMinipoolQueue, rocketDAOProtocolSettingsDeposit); } else { // Then assign megapools _assignMegapoolsByDeposit(rocketDAOProtocolSettingsDeposit); } } /// @dev Assigns a number of minipools based on `msg.value` function _assignMinipoolsByDeposit(RocketMinipoolQueueInterface _rocketMinipoolQueue, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Load contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Calculate the number of minipools to assign uint256 maxAssignments = _rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments(); uint256 variableDepositAmount = rocketDAOProtocolSettingsMinipool.getVariableDepositAmount(); uint256 scalingCount = msg.value / variableDepositAmount; uint256 totalEthCount = getBalance() / variableDepositAmount; uint256 assignments = _rocketDAOProtocolSettingsDeposit.getMaximumDepositSocialisedAssignments() + scalingCount; if (assignments > totalEthCount) { assignments = totalEthCount; } if (assignments > maxAssignments) { assignments = maxAssignments; } if (assignments > 0) { _assignMinipools(assignments, _rocketDAOProtocolSettingsDeposit); } } /// @dev Assigns a number of megapools based on `msg.value` function _assignMegapoolsByDeposit(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Calculate the number of megapool validators to assign uint256 maxAssignments = _rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments(); uint256 scalingCount = msg.value / 32 ether; uint256 assignments = _rocketDAOProtocolSettingsDeposit.getMaximumDepositSocialisedAssignments() + scalingCount; if (assignments > maxAssignments) { assignments = maxAssignments; } if (assignments > 0) { _assignMegapools(assignments, _rocketDAOProtocolSettingsDeposit); } } /// @dev Assigns up to `_count` number of minipools /// @param _count Maximum number of entries to assign function _assignMinipools(uint256 _count, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Get contracts RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Calculate max possible assignments based on current balance uint256 variableDepositAmount = rocketDAOProtocolSettingsMinipool.getVariableDepositAmount(); uint256 maxPossible = getBalance() / variableDepositAmount; if (maxPossible == 0) { return; } if (_count > maxPossible) { _count = maxPossible; } // Dequeue minipools address[] memory minipools = rocketMinipoolQueue.dequeueMinipools(_count); if (minipools.length > 0) { // Withdraw ETH from vault uint256 totalEther = minipools.length * variableDepositAmount; rocketVault.withdrawEther(totalEther); uint256 nodeBalanceUsed = 0; // Loop over minipools and deposit the amount required to reach launch balance for (uint256 i = 0; i < minipools.length; ++i) { RocketMinipoolInterface minipool = RocketMinipoolInterface(minipools[i]); // Assign deposit to minipool minipool.deposit{value: variableDepositAmount}(); nodeBalanceUsed = nodeBalanceUsed + minipool.getNodeTopUpValue(); // Emit deposit assigned event emit DepositAssigned(minipools[i], variableDepositAmount, block.timestamp); } // Decrease node balance subUint(nodeBalanceKey, nodeBalanceUsed); } } /// @dev Assigns up to `_count` number of megapools /// @param _count Maximum number of entries to assign function _assignMegapools(uint256 _count, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Get contracts LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); // Get required inputs uint256 expressQueueLength = linkedListStorage.getLength(expressQueueNamespace); uint256 standardQueueLength = linkedListStorage.getLength(standardQueueNamespace); uint256 queueIndex = getUint(queueIndexKey); uint256 expressQueueRate = _rocketDAOProtocolSettingsDeposit.getExpressQueueRate(); // Keep track of changes to applied at the end uint256 nodeBalanceUsed = 0; uint256 totalSent = 0; uint256 vaultBalance = getBalance(); // Keep track of whether heads move bool expressHeadMoved = false; bool standardHeadMoved = false; // Iterate over maximum of `_count` entries for (uint256 i = 0; i < _count; i++) { if (expressQueueLength == 0 && standardQueueLength == 0) { break; } // Determine if we are assigning an express queue entry bool express = queueIndex % (expressQueueRate + 1) != expressQueueRate; if (express && expressQueueLength == 0) { express = false; } if (!express && standardQueueLength == 0) { express = true; } // Get the entry bytes32 namespace = getQueueNamespace(express); LinkedListStorageInterface.DepositQueueValue memory head = linkedListStorage.peekItem(namespace); uint256 ethRequired = head.requestedValue * milliToWei; // Check if we have enough available to assign if (vaultBalance < ethRequired) { break; } // Withdraw the funds from the vault rocketVault.withdrawEther(ethRequired); vaultBalance -= ethRequired; // Assign funds and dequeue megapool RocketMegapoolInterface(head.receiver).assignFunds{value: ethRequired}(head.validatorId); emit FundsAssigned(head.receiver, ethRequired, block.timestamp); linkedListStorage.dequeueItem(namespace); // Account for node balance unchecked { // Infeasible overflows and impossible underflows nodeBalanceUsed += head.suppliedValue * milliToWei; totalSent += ethRequired; // Update counts for next iteration queueIndex += 1; if (express) { expressQueueLength -= 1; expressHeadMoved = true; } else { standardQueueLength -= 1; standardHeadMoved = true; } } } // Store state changes subUint(nodeBalanceKey, nodeBalanceUsed); setUint(queueIndexKey, queueIndex); subUint(requestedTotalKey, totalSent); _setQueueMoved(expressHeadMoved, standardHeadMoved); } /// @dev Stores block number when the queues moved function _setQueueMoved(bool expressHeadMoved, bool standardHeadMoved) internal { uint256 packed = getUint(queueMovedKey); uint128 express = expressHeadMoved ? uint128(block.number) : uint128(packed >> 0); uint128 standard = standardHeadMoved ? uint128(block.number) : uint128(packed >> 128); packed = express << 0; packed |= uint256(standard) << 128; setUint(queueMovedKey, packed); } /// @dev Withdraw excess deposit pool balance for rETH collateral /// @param _amount The amount of excess ETH to withdraw function withdrawExcessBalance(uint256 _amount) override external onlyThisLatestContract onlyLatestContract("rocketTokenRETH", msg.sender) { // Check amount require(_amount <= getExcessBalance(), "Insufficient excess balance for withdrawal"); // Withdraw ETH from vault rocketVault.withdrawEther(_amount); // Transfer to rETH contract rocketTokenRETH.depositExcess{value: _amount}(); // Emit excess withdrawn event emit ExcessWithdrawn(msg.sender, _amount, block.timestamp); } /// @notice Requests funds from the deposit queue by a megapool, places the request in the relevant queue /// @param _bondAmount The bond amount supplied by the NO for the fund request /// @param _validatorId The megapool-managed ID of the validator requesting funds /// @param _amount The amount of ETH requested by the node operator /// @param _expressQueue Whether to consume an express ticket to be placed in the express queue function requestFunds(uint256 _bondAmount, uint32 _validatorId, uint256 _amount, bool _expressQueue) external onlyRegisteredMegapool(msg.sender) onlyThisLatestContract { // Validate arguments require(_bondAmount % milliToWei == 0, "Invalid supplied amount"); require(_amount % milliToWei == 0, "Invalid requested amount"); // Use an express ticket if requested address nodeAddress = RocketMegapoolInterface(msg.sender).getNodeAddress(); RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); if (_expressQueue) { rocketNodeManager.useExpressTicket(nodeAddress); } else { rocketNodeManager.provisionExpressTickets(nodeAddress); } // Enqueue megapool bytes32 namespace = getQueueNamespace(_expressQueue); LinkedListStorageInterface.DepositQueueValue memory value = LinkedListStorageInterface.DepositQueueValue({ receiver: msg.sender, // Megapool address validatorId: _validatorId, // Incrementing id per validator in a megapool suppliedValue: SafeCast.toUint32(_bondAmount / milliToWei), // NO bond amount requestedValue: SafeCast.toUint32(_amount / milliToWei) // Amount being requested }); LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); linkedListStorage.enqueueItem(namespace, value); // Increase requested balance addUint(requestedTotalKey, _amount); // Check if head moved if (_expressQueue) { uint256 expressQueueLength = linkedListStorage.getLength(expressQueueNamespace); if (expressQueueLength == 1) { _setQueueMoved(true, false); } } else { uint256 standardQueueLength = linkedListStorage.getLength(standardQueueNamespace); if (standardQueueLength == 1) { _setQueueMoved(false, true); } } { // Update collateral balances _increaseETHBonded(nodeAddress, _bondAmount); _increaseETHBorrowed(nodeAddress, _amount - _bondAmount); } // Emit event emit FundsRequested(msg.sender, _validatorId, _amount, _expressQueue, block.timestamp); } /// @dev Called from a megapool to remove an entry in the validator queue and returns funds to node by credit mechanism /// @param _validatorId Internal ID of the validator to be removed /// @param _expressQueue Whether the entry is in the express queue or not function exitQueue(address _nodeAddress, uint32 _validatorId, bool _expressQueue) external onlyRegisteredMegapool(msg.sender) onlyThisLatestContract { LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); LinkedListStorageInterface.DepositQueueKey memory key = LinkedListStorageInterface.DepositQueueKey({ receiver: msg.sender, validatorId: _validatorId }); bytes32 namespace = getQueueNamespace(_expressQueue); uint256 index = linkedListStorage.getIndexOf(namespace, key); LinkedListStorageInterface.DepositQueueValue memory value = linkedListStorage.getItem(namespace, index); bool isAtHead = linkedListStorage.getHeadIndex(namespace) == index; linkedListStorage.removeItem(namespace, key); // Perform balance accounting subUint(requestedTotalKey, value.requestedValue * milliToWei); if (_expressQueue) { // Refund express ticket RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); rocketNodeManager.refundExpressTicket(_nodeAddress); // Update head moved block if (isAtHead) { _setQueueMoved(true, false); } } else { // Update head moved block if (isAtHead) { _setQueueMoved(false, true); } } // Remove bond from node balance uint256 nodeBalanceUsed = value.suppliedValue * milliToWei; subUint(nodeBalanceKey, nodeBalanceUsed); // Emit event emit QueueExited(_nodeAddress, block.timestamp); } /// @dev Called from megapool to increase a node operator's credit function applyCredit(address _nodeAddress, uint256 _amount) override external onlyRegisteredMegapool(msg.sender) onlyThisLatestContract { // Add to node's credit for the amount supplied addUint(keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress)), _amount); } /// @notice Allows node operator to withdraw any ETH credit they have as rETH /// @param _amount Amount in ETH to withdraw function withdrawCredit(uint256 _amount) override external onlyRegisteredNode(msg.sender) onlyThisLatestContract { address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(msg.sender); _withdrawCreditFor(msg.sender, withdrawalAddress, _amount); } /// @notice Allows node operator to withdraw any ETH credit they have as rETH from their withdrawal address /// @param _nodeAddress Address of the node operator /// @param _amount Amount in ETH to withdraw function withdrawCreditFor(address _nodeAddress, uint256 _amount) override public onlyRegisteredNode(_nodeAddress) onlyThisLatestContract { // Check caller address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); require(msg.sender == withdrawalAddress, "Must be called from withdrawal address"); // Withdraw credit _withdrawCreditFor(_nodeAddress, withdrawalAddress, _amount); } /// @dev Withdraws credit from a given node operator as rETH function _withdrawCreditFor(address _nodeAddress, address _recipient, uint256 _amount) internal { // Check deposits are enabled RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled"); // Ensure no debt exists on the node operator's megapool RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); require(rocketMegapoolFactory.getMegapoolDeployed(_nodeAddress), "Megapool must be deployed"); RocketMegapoolDelegateInterface megapool = RocketMegapoolDelegateInterface(rocketMegapoolFactory.getExpectedAddress(_nodeAddress)); require(megapool.getDebt() == 0, "Cannot withdraw credit while debt exists"); // Check node operator has sufficient credit bytes32 creditKey = keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress)); uint256 credit = getUint(creditKey); require(credit >= _amount, "Amount exceeds credit available"); // Account for balance changes subUint(creditKey, _amount); // Note: The funds are already stored in RocketVault under RocketDepositPool so no ETH transfer is required RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); // Calculate deposit fee unchecked { // depositFee < msg.value uint256 depositFee = _amount * rocketDAOProtocolSettingsDeposit.getDepositFee() / calcBase; uint256 depositNet = _amount - depositFee; // Mint rETH to recipient address rocketTokenRETH.mint(depositNet, _recipient); } // Emit event emit CreditWithdrawn(_nodeAddress, _amount, block.timestamp); } /// @notice Gets the receiver next to be assigned and whether it can be assigned immediately /// @dev During the transition period from the legacy minipool queue, this will always return null address /// @return receiver Address of the receiver of the next assignment or null address for an empty queue /// @return assignmentPossible Whether there is enough funds in the pool to perform an assignment now /// @return headMovedBlock The block at which the receiver entered the top of the queue function getQueueTop() override external view returns (address receiver, bool assignmentPossible, uint256 headMovedBlock) { // If legacy queue is still being processed, return null address AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); if (addressQueueStorage.getLength(queueKeyMinipoolVariable) > 0) { return (address(0x0), false, 0); } // Get contracts LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); uint256 expressQueueLength = linkedListStorage.getLength(expressQueueNamespace); uint256 standardQueueLength = linkedListStorage.getLength(standardQueueNamespace); // If both queues are empty, return null address if (expressQueueLength == 0 && standardQueueLength == 0) { return (address(0x0), false, 0); } uint256 queueIndex = getUint(queueIndexKey); uint256 expressQueueRate = rocketDAOProtocolSettingsDeposit.getExpressQueueRate(); bool express = queueIndex % (expressQueueRate + 1) != expressQueueRate; if (express && expressQueueLength == 0) { express = false; } if (!express && standardQueueLength == 0) { express = true; } // Check if enough value is in the deposit pool to assign the requested value bytes32 namespace = getQueueNamespace(express); LinkedListStorageInterface.DepositQueueValue memory head = linkedListStorage.peekItem(namespace); assignmentPossible = rocketVault.balanceOf("rocketDepositPool") >= head.requestedValue * milliToWei; // Check assignments are enabled if (!rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { assignmentPossible = false; } // Retrieve the block at which the entry at the top of the queue got to that position uint256 packed = getUint(queueMovedKey); if (express) { headMovedBlock = uint128(packed); } else { headMovedBlock = uint128(packed >> 128); } return (head.receiver, assignmentPossible, headMovedBlock); } /// @notice Retrieves the queue index (used for deciding whether to assign express or standard queue next) function getQueueIndex() override external view returns (uint256) { return getUint(queueIndexKey); } /// @notice Returns the number of minipools in the queue function getMinipoolQueueLength() override public view returns (uint256) { AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); return addressQueueStorage.getLength(queueKeyMinipoolVariable); } /// @notice Returns the number of megapool validators in the express queue function getExpressQueueLength() override public view returns (uint256) { LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); return linkedListStorage.getLength(expressQueueNamespace); } /// @notice Returns the number of megapool validators in the standard queue function getStandardQueueLength() override public view returns (uint256) { LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); return linkedListStorage.getLength(standardQueueNamespace); } /// @notice Returns the total number of minipools/megapools in the queue function getTotalQueueLength() override external view returns (uint256) { return getMinipoolQueueLength() + getExpressQueueLength() + getStandardQueueLength(); } /// @dev Convenience method to return queue key for express and non-express queues function getQueueNamespace(bool _expressQueue) internal pure returns (bytes32) { if (_expressQueue) { return expressQueueNamespace; } return standardQueueNamespace; } /// @dev Called by a megapool during a bond reduction to adjust its capital ratio function reduceBond(address _nodeAddress, uint256 _amount) override external onlyRegisteredMegapool(msg.sender) onlyThisLatestContract { // Update collateral balances _increaseETHBorrowed(_nodeAddress, _amount); _decreaseETHBonded(_nodeAddress, _amount); } /// @dev Called by a megapool when exiting to handle change in capital ratio function fundsReturned(address _nodeAddress, uint256 _nodeAmount, uint256 _userAmount) override external onlyRegisteredMegapool(msg.sender) onlyThisLatestContract { // Update collateral balances _decreaseETHBonded(_nodeAddress, _nodeAmount); _decreaseETHBorrowed(_nodeAddress, _userAmount); } /// @dev Increases the amount of ETH supplied by a node operator as bond function _increaseETHBonded(address _nodeAddress, uint256 _amount) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("megapool.eth.provided.node.amount", _nodeAddress)); uint256 ethBonded = uint256(rocketNetworkSnapshots.latestValue(key)) + _amount; rocketNetworkSnapshots.push(key, uint224(ethBonded)); } /// @dev Increases the amount of ETH borrowed by a node operator function _increaseETHBorrowed(address _nodeAddress, uint256 _amount) internal { bytes32 key = keccak256(abi.encodePacked("megapool.eth.matched.node.amount", _nodeAddress)); addUint(key, _amount); } /// @dev Decreases the amount of ETH bonded by a node operator as bond function _decreaseETHBonded(address _nodeAddress, uint256 _amount) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("megapool.eth.provided.node.amount", _nodeAddress)); uint256 ethBonded = uint256(rocketNetworkSnapshots.latestValue(key)) - _amount; rocketNetworkSnapshots.push(key, uint224(ethBonded)); } /// @dev Decreases the amount of ETH borrowed by a node operator function _decreaseETHBorrowed(address _nodeAddress, uint256 _amount) internal { bytes32 key = keccak256(abi.encodePacked("megapool.eth.matched.node.amount", _nodeAddress)); subUint(key, _amount); } } ================================================ FILE: contracts/contract/helper/BeaconStateVerifierMock.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../interface/RocketStorageInterface.sol"; import "../../interface/util/BeaconStateVerifierInterface.sol"; import {BeaconStateVerifier} from "../util/BeaconStateVerifier.sol"; /// @dev NOT USED IN PRODUCTION - This contract only exists to bypass state proofs during tests contract BeaconStateVerifierMock is BeaconStateVerifierInterface { bool private disabled = false; BeaconStateVerifierInterface private immutable verifier; mapping(uint256 => bytes32) internal beaconRoots; constructor(RocketStorageInterface _rocketStorageAddress) { // Set to mainnet values for use in unit tests with real proofs uint64[5] memory forkSlots; forkSlots[0] = 74240 * 32; forkSlots[1] = 144896 * 32; forkSlots[2] = 194048 * 32; forkSlots[3] = 269568 * 32; forkSlots[4] = 364032 * 32; verifier = new BeaconStateVerifier(_rocketStorageAddress, 8192, forkSlots, address(this), 1606824023, 0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95); } function setDisabled(bool _disabled) external { disabled = _disabled; } function verifyValidator(uint64 _slotTimestamp, uint64 _slot, ValidatorProof calldata _proof) override external view returns(bool) { if (disabled) { return true; } return verifier.verifyValidator(_slotTimestamp, _slot, _proof); } function verifyWithdrawal(uint64 _slotTimestamp, uint64 _slot, WithdrawalProof calldata _proof) override external view returns(bool) { if (disabled) { return true; } return verifier.verifyWithdrawal(_slotTimestamp, _slot, _proof); } function verifySlot(uint64 _slotTimestamp, SlotProof calldata _proof) override external view returns(bool) { if (disabled) { return true; } return verifier.verifySlot(_slotTimestamp, _proof); } function setBlockRoot(uint256 _timestamp, bytes32 _root) external { beaconRoots[_timestamp] = _root; } fallback(bytes calldata _input) external returns (bytes memory) { uint256 timestamp = abi.decode(_input, (uint256)); if (beaconRoots[timestamp] != 0) { return abi.encode(beaconRoots[timestamp]); } revert(); } } ================================================ FILE: contracts/contract/helper/MegapoolUpgradeHelper.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../RocketBase.sol"; import {RocketMegapoolFactoryInterface} from "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; /// @dev NOT USED IN PRODUCTION - Helper contract used to insert arbitrary snapshots in for testing contract MegapoolUpgradeHelper is RocketBase { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { } function upgradeDelegate(address _newDelegate) external onlyGuardian { RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); rocketMegapoolFactory.upgradeDelegate(_newDelegate); } } ================================================ FILE: contracts/contract/helper/PenaltyTest.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/minipool/RocketMinipoolPenaltyInterface.sol"; // THIS CONTRACT IS NOT DEPLOYED TO MAINNET // Helper contract used in unit tests that can set the penalty rate on a minipool (a feature that will be implemented at a later time) contract PenaltyTest is RocketBase { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { } // Sets the penalty rate for the given minipool function setPenaltyRate(address _minipoolAddress, uint256 _rate) external { RocketMinipoolPenaltyInterface rocketMinipoolPenalty = RocketMinipoolPenaltyInterface(getContractAddress("rocketMinipoolPenalty")); rocketMinipoolPenalty.setPenaltyRate(_minipoolAddress, _rate); } } ================================================ FILE: contracts/contract/helper/RevertOnTransfer.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; /// @notice Helper contract to simulate malicious node withdrawal address or withdrawal address contract RevertOnTransfer { bool public enabled = true; function setEnabled(bool _enabled) external { enabled = _enabled; } receive() external payable { require(!enabled); } function call(address _address, bytes calldata _payload) external payable { (bool success,) = _address.call{value: msg.value}(_payload); require(success, "Failed to transfer"); } } ================================================ FILE: contracts/contract/helper/SnapshotTest.sol ================================================ pragma solidity 0.8.30; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/network/RocketNetworkSnapshotsInterface.sol"; // THIS CONTRACT IS NOT DEPLOYED TO MAINNET // Helper contract used to insert arbitrary snapshots in for testing contract SnapshotTest is RocketBase { RocketNetworkSnapshotsInterface snapshots; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { snapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); } function push(string calldata _key, uint224 _value) external { bytes32 key = keccak256(abi.encodePacked(_key)); snapshots.push(key, _value); } function lookup(string calldata _key, uint32 _block) external view returns (uint224){ bytes32 key = keccak256(abi.encodePacked(_key)); return snapshots.lookup(key, _block); } function lookupRecent(string calldata _key, uint32 _block, uint256 _recency) external view returns (uint224) { bytes32 key = keccak256(abi.encodePacked(_key)); return snapshots.lookupRecent(key, _block, _recency); } function lookupGas(string calldata _key, uint32 _block) external view returns (uint256) { bytes32 key = keccak256(abi.encodePacked(_key)); uint256 gasBefore = gasleft(); snapshots.lookup(key, _block); return gasBefore - gasleft(); } function lookupRecentGas(string calldata _key, uint32 _block, uint256 _recency) external view returns (uint256) { bytes32 key = keccak256(abi.encodePacked(_key)); uint256 gasBefore = gasleft(); snapshots.lookupRecent(key, _block, _recency); return gasBefore - gasleft(); } } ================================================ FILE: contracts/contract/helper/SnapshotTimeTest.sol ================================================ pragma solidity 0.8.30; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/network/RocketNetworkSnapshotsTimeInterface.sol"; // THIS CONTRACT IS NOT DEPLOYED TO MAINNET // Helper contract used to insert arbitrary snapshots in for testing contract SnapshotTimeTest is RocketBase { RocketNetworkSnapshotsTimeInterface snapshots; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { snapshots = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); } function push(string calldata _key, uint192 _value) external { bytes32 key = keccak256(abi.encodePacked(_key)); snapshots.push(key, _value); } function lookup(string calldata _key, uint64 _time) external view returns (uint192){ bytes32 key = keccak256(abi.encodePacked(_key)); return snapshots.lookup(key, _time); } function lookupRecent(string calldata _key, uint64 _time, uint256 _recency) external view returns (uint192) { bytes32 key = keccak256(abi.encodePacked(_key)); return snapshots.lookupRecent(key, _time, _recency); } function lookupGas(string calldata _key, uint64 _time) external view returns (uint256) { bytes32 key = keccak256(abi.encodePacked(_key)); uint256 gasBefore = gasleft(); snapshots.lookup(key, _time); return gasBefore - gasleft(); } function lookupRecentGas(string calldata _key, uint64 _time, uint256 _recency) external view returns (uint256) { bytes32 key = keccak256(abi.encodePacked(_key)); uint256 gasBefore = gasleft(); snapshots.lookupRecent(key, _time, _recency); return gasBefore - gasleft(); } } ================================================ FILE: contracts/contract/helper/StakeHelper.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../../interface/RocketVaultInterface.sol"; import "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol"; /// @dev NOT USED IN PRODUCTION - Helper contract to manually adjust state for tests contract StakeHelper is RocketBase { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { } function lockRPL(address _nodeAddress, uint256 _amount) external { RocketNodeStakingInterface rocketNodeStakingInterface = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStakingInterface.lockRPL(_nodeAddress, _amount); } function unlockRPL(address _nodeAddress, uint256 _amount) external { RocketNodeStakingInterface rocketNodeStakingInterface = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStakingInterface.unlockRPL(_nodeAddress, _amount); } function transferRPL(address _from, address _to, uint256 _amount) external { RocketNodeStakingInterface rocketNodeStakingInterface = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStakingInterface.transferRPL(_from, _to, _amount); } function burnRPL(address _from, uint256 _amount) external { RocketNodeStakingInterface rocketNodeStakingInterface = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStakingInterface.burnRPL(_from, _amount); } function addLegacyStakedRPL(address _nodeAddress, uint256 _amount) external { bytes32 migratedKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.migrated", _nodeAddress)); require(!getBool(migratedKey), "Cannot set once migrated"); // Get contracts RocketTokenRPLInterface rplToken = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Transfer RPL tokens require(rplToken.transferFrom(msg.sender, address(this), _amount), "Could not transfer RPL to staking contract"); // Deposit RPL tokens to vault require(rplToken.approve(address(rocketVault), _amount), "Could not approve vault RPL deposit"); rocketVault.depositToken("rocketNodeStaking", rplToken, _amount); // Adjust value RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); uint224 amount = rocketNetworkSnapshots.latestValue(key); rocketNetworkSnapshots.push(key, uint224(_amount) + amount); // Update total bytes32 totalKey = keccak256(abi.encodePacked("rpl.staked.total.amount")); addUint(totalKey, _amount); } } ================================================ FILE: contracts/contract/helper/StorageHelper.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; /// @dev NOT USED IN PRODUCTION - Helper contract used to perform manual edits to storage contract StorageHelper { RocketStorageInterface immutable public rocketStorage; modifier onlyGuardian() { require(msg.sender == rocketStorage.getGuardian(), "Account is not a temporary guardian"); _; } // Construct constructor(RocketStorageInterface _rocketStorageAddress) { rocketStorage = _rocketStorageAddress; } function getAddress(bytes32 _key) external view returns (address) { return rocketStorage.getAddress(_key); } function getUint(bytes32 _key) external view returns (uint) { return rocketStorage.getUint(_key); } function getString(bytes32 _key) external view returns (string memory) { return rocketStorage.getString(_key); } function getBytes(bytes32 _key) external view returns (bytes memory) { return rocketStorage.getBytes(_key); } function getBool(bytes32 _key) external view returns (bool) { return rocketStorage.getBool(_key); } function getInt(bytes32 _key) external view returns (int) { return rocketStorage.getInt(_key); } function getBytes32(bytes32 _key) external view returns (bytes32) { return rocketStorage.getBytes32(_key); } function setAddress(bytes32 _key, address _value) external onlyGuardian { rocketStorage.setAddress(_key, _value); } function setUint(bytes32 _key, uint _value) external onlyGuardian { rocketStorage.setUint(_key, _value); } function setString(bytes32 _key, string memory _value) external onlyGuardian { rocketStorage.setString(_key, _value); } function setBytes(bytes32 _key, bytes memory _value) external onlyGuardian { rocketStorage.setBytes(_key, _value); } function setBool(bytes32 _key, bool _value) external onlyGuardian { rocketStorage.setBool(_key, _value); } function setInt(bytes32 _key, int _value) external onlyGuardian { rocketStorage.setInt(_key, _value); } function setBytes32(bytes32 _key, bytes32 _value) external onlyGuardian { rocketStorage.setBytes32(_key, _value); } /// @dev Storage delete methods function deleteAddress(bytes32 _key) external onlyGuardian { rocketStorage.deleteAddress(_key); } function deleteUint(bytes32 _key) external onlyGuardian { rocketStorage.deleteUint(_key); } function deleteString(bytes32 _key) external onlyGuardian { rocketStorage.deleteString(_key); } function deleteBytes(bytes32 _key) external onlyGuardian { rocketStorage.deleteBytes(_key); } function deleteBool(bytes32 _key) external onlyGuardian { rocketStorage.deleteBool(_key); } function deleteInt(bytes32 _key) external onlyGuardian { rocketStorage.deleteInt(_key); } function deleteBytes32(bytes32 _key) external onlyGuardian { rocketStorage.deleteBytes32(_key); } function addUint(bytes32 _key, uint256 _amount) external onlyGuardian { rocketStorage.addUint(_key, _amount); } function subUint(bytes32 _key, uint256 _amount) external onlyGuardian { rocketStorage.subUint(_key, _amount); } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolDelegate.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {DepositInterface} from "../../interface/casper/DepositInterface.sol"; import {RocketDAOProtocolSettingsMegapoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMegapoolInterface.sol"; import {RocketDepositPoolInterface} from "../../interface/deposit/RocketDepositPoolInterface.sol"; import {RocketMegapoolDelegateInterface} from "../../interface/megapool/RocketMegapoolDelegateInterface.sol"; import {RocketNetworkRevenuesInterface} from "../../interface/network/RocketNetworkRevenuesInterface.sol"; import {RocketNodeDepositInterface} from "../../interface/node/RocketNodeDepositInterface.sol"; import {RocketRewardsPoolInterface} from "../../interface/rewards/RocketRewardsPoolInterface.sol"; import {RocketTokenRETHInterface} from "../../interface/token/RocketTokenRETHInterface.sol"; import {RocketMegapoolDelegateBase} from "./RocketMegapoolDelegateBase.sol"; import {RocketMegapoolStorageLayout} from "./RocketMegapoolStorageLayout.sol"; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; /// @notice This contract manages multiple validators belonging to an individual node operator. /// It serves as the withdrawal credentials for all Beacon Chain validators managed by it. contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDelegateInterface { // Constants uint256 constant internal prestakeValue = 1 ether; uint256 constant internal fullDepositValue = 32 ether; uint256 constant internal milliToWei = 10 ** 15; uint256 constant internal calcBase = 1 ether; // Events event MegapoolValidatorEnqueued(uint256 indexed validatorId, uint256 time); event MegapoolValidatorDequeued(uint256 indexed validatorId, uint256 time); event MegapoolValidatorAssigned(uint256 indexed validatorId, uint256 time); event MegapoolValidatorExited(uint32 indexed validatorId, uint256 time); event MegapoolValidatorExiting(uint256 indexed validatorId, uint256 time); event MegapoolValidatorLocked(uint256 indexed validatorId, uint256 time); event MegapoolValidatorUnlocked(uint256 indexed validatorId, uint256 time); event MegapoolValidatorDissolved(uint256 indexed validatorId, uint256 time); event MegapoolValidatorStaked(uint256 indexed validatorId, uint256 time); event MegapoolPenaltyApplied(uint256 amount, uint256 time); event MegapoolDebtIncreased(uint256 amount, uint256 time); event MegapoolDebtReduced(uint256 amount, uint256 time); event MegapoolBondReduced(uint256 amount, uint256 time); event RewardsDistributed(uint256 nodeAmount, uint256 voterAmount, uint256 rethAmount, uint256 protocolDaoAmount, uint256 time); event RewardsClaimed(uint256 amount, uint256 time); // Immutables bytes32 immutable internal rocketDepositPoolKey; bytes32 immutable internal rocketMegapoolManagerKey; bytes32 immutable internal rocketNodeDepositKey; address payable immutable internal rocketTokenRETH; DepositInterface immutable internal casperDeposit; modifier onlyRocketMegapoolManager() { require(msg.sender == rocketStorage.getAddress(rocketMegapoolManagerKey), "Invalid or outdated contract"); _; } modifier onlyRocketNodeDeposit() { require(msg.sender == rocketStorage.getAddress(rocketNodeDepositKey), "Invalid or outdated contract"); _; } /// @notice Constructor /// @param _rocketStorageAddress Address of the deployments RocketStorage constructor(RocketStorageInterface _rocketStorageAddress) RocketMegapoolDelegateBase(_rocketStorageAddress, 1) { // Precompute static storage keys rocketDepositPoolKey = keccak256(abi.encodePacked("contract.address", "rocketDepositPool")); rocketMegapoolManagerKey = keccak256(abi.encodePacked("contract.address", "rocketMegapoolManager")); rocketNodeDepositKey = keccak256(abi.encodePacked("contract.address", "rocketNodeDeposit")); // Prefetch immutable contracts rocketTokenRETH = payable(getContractAddress("rocketTokenRETH")); casperDeposit = DepositInterface(getContractAddress("casperDeposit")); } /// @notice Gets the Node address associated to this megapool function getNodeAddress() public override view returns (address) { return nodeAddress; } /// @notice Returns the number of validators created for this megapool function getValidatorCount() override external view returns (uint32) { return numValidators; } /// @notice Returns the number of validators that are considered for bond requirement function getActiveValidatorCount() override public view returns (uint32) { return numValidators - numInactiveValidators; } /// @notice Returns the number of validators currently exiting function getExitingValidatorCount() external view returns (uint32) { return numExitingValidators; } /// @notice Returns the number of validators locked by a exit challenge function getLockedValidatorCount() external view returns (uint32) { return numLockedValidators; } /// @notice Returns information about a given validator function getValidatorInfo(uint32 _validatorId) override external view returns (ValidatorInfo memory) { require(_validatorId < numValidators, "Validator does not exist"); return validators[_validatorId]; } /// @notice Returns pubkey for a given validator function getValidatorPubkey(uint32 _validatorId) override external view returns (bytes memory) { require(_validatorId < numValidators, "Validator does not exist"); return pubkeys[_validatorId]; } /// @notice Returns both validator information and pubkey /// @param _validatorId Internal ID of the validator to query function getValidatorInfoAndPubkey(uint32 _validatorId) override external view returns (ValidatorInfo memory info, bytes memory pubkey) { require(_validatorId < numValidators, "Validator does not exist"); info = validators[_validatorId]; pubkey = pubkeys[_validatorId]; } /// @notice Returns the amount of ETH temporarily held in this contract from the protocol ready to be staked function getAssignedValue() override external view returns (uint256) { return assignedValue; } /// @notice Returns the amount of ETH the node operator owes the protocol function getDebt() override external view returns (uint256) { return debt; } /// @notice Returns the amount of ETH available to refund to the node operator function getRefundValue() override external view returns (uint256) { return refundValue; } /// @notice Returns the amount of ETH supplied by the node operator (Bonded ETH) function getNodeBond() override external view returns (uint256) { return nodeBond; } /// @notice Returns the amount of ETH capital provided by the protocol (Borrowed ETH) function getUserCapital() override external view returns (uint256) { return userCapital; } /// @notice Returns the amount of ETH bond provided by the node operator waiting in the queue for assignment function getNodeQueuedBond() override external view returns (uint256) { return nodeQueuedBond; } /// @notice Returns the amount of ETH capital provided by the protocol waiting in the queue for assignment function getUserQueuedCapital() override public view returns (uint256) { return userQueuedCapital; } /// @notice Returns the amount in wei of pending rewards ready to be distributed function getPendingRewards() override public view returns (uint256) { return address(this).balance - refundValue - assignedValue; } /// @notice Returns the block timestamp of the last distribution performed function getLastDistributionTime() override external view returns (uint256) { return lastDistributionTime; } /// @notice Returns the expected withdrawal credentials for any validator within this megapool function getWithdrawalCredentials() override public view returns (bytes32) { return bytes32((uint256(0x01) << 248) | uint256(uint160(address(this)))); } /// @notice Returns the bond requirement for a new validator function getNewValidatorBondRequirement() override public view returns (uint256) { RocketNodeDepositInterface rocketNodeDeposit = _getRocketNodeDeposit(); uint256 newBondRequirement = rocketNodeDeposit.getBondRequirement(getActiveValidatorCount() + 1); uint256 effectiveBond = nodeBond + nodeQueuedBond; if (newBondRequirement > effectiveBond) { // Clamp new bond requirement between 1 - 32 ETH if (newBondRequirement - effectiveBond < prestakeValue) { return prestakeValue; } else { uint256 bondRequirement = newBondRequirement - effectiveBond; if (bondRequirement > fullDepositValue) { bondRequirement = fullDepositValue; } return bondRequirement; } } else { return prestakeValue; } } /// @notice Creates a new validator for this megapool /// @param _bondAmount The bond amount supplied by the node operator /// @param _useExpressTicket If an express ticket should be used /// @param _validatorPubkey The pubkey of the new validator /// @param _validatorSignature A signature over the deposit data root /// @param _depositDataRoot Merkle root of the deposit data function newValidator(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external onlyRocketNodeDeposit { // Check bond and debt requirements require(_bondAmount == getNewValidatorBondRequirement(), "Bond requirement not met"); require(debt == 0, "Cannot create validator while debt exists"); // Setup new validator RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool(); uint32 validatorId = numValidators; unchecked { // Infeasible overflow numValidators += 1; } { ValidatorInfo memory validator; validator.inQueue = true; validator.lastRequestedBond = SafeCast.toUint32(_bondAmount / milliToWei); validator.lastRequestedValue = SafeCast.toUint32(fullDepositValue / milliToWei); validator.expressUsed = _useExpressTicket; validators[validatorId] = validator; } // Store prestake data and pubkey prestakeSignatures[validatorId] = _validatorSignature; pubkeys[validatorId] = _validatorPubkey; // Compute and verify supplied deposit data root is correct // Note: We check this here to ensure the deposit contract will not revert when executing prestake bytes32 depositDataRoot = _computeDepositDataRoot(_validatorPubkey, _validatorSignature, SafeCast.toUint64(prestakeValue / 1 gwei)); require(depositDataRoot == _depositDataRoot, "Invalid deposit data root"); // Increase queued capital balances userQueuedCapital += fullDepositValue - _bondAmount; nodeQueuedBond += _bondAmount; // Request full deposit amount from deposit pool rocketDepositPool.requestFunds(_bondAmount, validatorId, fullDepositValue, _useExpressTicket); // Emit event emit MegapoolValidatorEnqueued(validatorId, block.timestamp); } /// @notice Removes a validator from the deposit queue /// @param _validatorId the validator ID function dequeue(uint32 _validatorId) external onlyMegapoolOwner { ValidatorInfo memory validator = validators[_validatorId]; // Validate validator status require(validator.inQueue, "Validator must be in queue"); uint256 requestedValue = uint256(validator.lastRequestedValue) * milliToWei; uint256 nodeValue = uint256(validator.lastRequestedBond) * milliToWei; uint256 userValue = requestedValue - nodeValue; // Dequeue validator from the deposit pool and issue credit RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool(); rocketDepositPool.exitQueue(nodeAddress, _validatorId, validator.expressUsed); rocketDepositPool.fundsReturned(nodeAddress, nodeValue, userValue); rocketDepositPool.applyCredit(nodeAddress, nodeValue); // Remove value from queued capital balances nodeQueuedBond -= nodeValue; userQueuedCapital -= userValue; // Increment inactive validator count unchecked { // Infeasible overflow numInactiveValidators += 1; } // Verify new bond requirement is met RocketNodeDepositInterface rocketNodeDeposit = _getRocketNodeDeposit(); uint256 newBondRequirement = rocketNodeDeposit.getBondRequirement(getActiveValidatorCount()); require(nodeBond + nodeQueuedBond >= newBondRequirement, "Bond requirement not met"); // Update validator state validator.inQueue = false; validator.expressUsed = false; validator.lastRequestedBond = 0; validator.lastRequestedValue = 0; validators[_validatorId] = validator; // Delete prestake signature delete prestakeSignatures[_validatorId]; // Emit event emit MegapoolValidatorDequeued(_validatorId, block.timestamp); } /// @notice Reduces this megapool's bond and applies credit if current bond exceeds requirement /// @param _amount Amount in ETH to reduce bond by function reduceBond(uint256 _amount) override external onlyMegapoolOwner { // Check pre-conditions require(_amount > 0, "Invalid amount"); require(debt == 0, "Cannot reduce bond with debt"); require(nodeQueuedBond == 0, "Cannot reduce bond with queued validators"); require(assignedValue == 0, "Cannot reduce bond with prestaked validators"); // Check bond requirements RocketNodeDepositInterface rocketNodeDeposit = _getRocketNodeDeposit(); uint256 newBondRequirement = rocketNodeDeposit.getBondRequirement(getActiveValidatorCount()); uint256 effectiveBond = nodeBond; require(effectiveBond > newBondRequirement, "Bond is at minimum"); unchecked { // Impossible underflow given effectiveBond > newBondRequirement uint256 maxReduce = effectiveBond - newBondRequirement; require(_amount <= maxReduce, "New bond is too low"); } // Reduce node bond nodeBond -= _amount; userCapital += _amount; // Snapshot capital ratio _snapshotCapitalRatio(); // Apply credit RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool(); rocketDepositPool.applyCredit(nodeAddress, _amount); rocketDepositPool.reduceBond(nodeAddress, _amount); // Emit event emit MegapoolBondReduced(_amount, block.timestamp); } /// @notice Accepts requested funds from the deposit pool /// @param _validatorId the validator ID function assignFunds(uint32 _validatorId) external payable onlyLatestContract("rocketDepositPool", msg.sender) { // Fetch validator data from storage ValidatorInfo memory validator = validators[_validatorId]; // Update validator status validator.inQueue = false; validator.inPrestake = true; validator.lastAssignmentTime = SafeCast.toUint32(block.timestamp); // Record value assigned from deposit pool (subtract prestakeValue as it is going to deposit contract now) validator.depositValue += SafeCast.toUint32(prestakeValue / milliToWei); assignedValue += msg.value - prestakeValue; validators[_validatorId] = validator; // Execute prestake operation bytes memory signature = prestakeSignatures[_validatorId]; bytes memory pubkey = pubkeys[_validatorId]; bytes32 depositDataRoot = _computeDepositDataRoot(pubkey, signature, SafeCast.toUint64(prestakeValue / 1 gwei)); casperDeposit.deposit{value: prestakeValue}(pubkey, abi.encodePacked(getWithdrawalCredentials()), signature, depositDataRoot); // Remove value from queued balances and add to staking values uint256 assignedUserCapital = (validator.lastRequestedValue - validator.lastRequestedBond) * milliToWei; uint256 assignedNodeBond = (validator.lastRequestedBond * milliToWei); userCapital += assignedUserCapital; nodeBond += assignedNodeBond; userQueuedCapital -= assignedUserCapital; nodeQueuedBond -= assignedNodeBond; // Store capital ratio on first assignment if (lastDistributionTime == 0) { _calculateAndSaveCapitalRatio(); } // Delete prestake signature for a small gas refund (no longer needed) delete prestakeSignatures[_validatorId]; // Emit event emit MegapoolValidatorAssigned(_validatorId, block.timestamp); } /// @notice Performs the remaining ETH deposit on the Beacon Chain /// @param _validatorId The internal ID of the validator in this megapool function stake(uint32 _validatorId) external onlyRocketMegapoolManager { // Retrieve validator from storage ValidatorInfo memory validator = validators[_validatorId]; // Validate validator status require(validator.inPrestake, "Validator must be pre-staked"); // Store last requested value for later uint32 lastRequestedValue = validator.lastRequestedValue; // Snapshot capital ratio _snapshotCapitalRatio(); // Account for assigned value uint256 assignedUsed = lastRequestedValue * milliToWei - prestakeValue; assignedValue -= assignedUsed; // Update validator status validator.staked = true; validator.inPrestake = false; validator.lastAssignmentTime = 0; validator.lastRequestedBond = 0; validator.lastRequestedValue = 0; validator.depositValue += SafeCast.toUint32(lastRequestedValue - prestakeValue / milliToWei); validators[_validatorId] = validator; // Perform remaining 31 ETH stake onto beaconchain // Note: Signature is not verified on subsequent deposits and we know the validator is valid due to state proof bytes memory signature = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; bytes memory pubkey = pubkeys[_validatorId]; bytes32 depositDataRoot = _computeDepositDataRoot(pubkey, signature, SafeCast.toUint64(assignedUsed / 1 gwei)); casperDeposit.deposit{value: assignedUsed}(pubkey, abi.encodePacked(getWithdrawalCredentials()), signature, depositDataRoot); // Emit event emit MegapoolValidatorStaked(_validatorId, block.timestamp); } /// @notice Dissolves a validator that has not staked within the required period /// @param _validatorId the validator ID to dissolve /// @dev "Time before dissolve" parameter must be respected if not called from RocketMegapoolManager function dissolveValidator(uint32 _validatorId) override external { // Retrieve validator from storage ValidatorInfo memory validator = validators[_validatorId]; // Check current status require(validator.inPrestake, "Validator not prestaked"); // Ensure time-before-dissolve period has passed before allowing proof-less dissolution RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); if (msg.sender != rocketStorage.getAddress(rocketMegapoolManagerKey)) { uint256 timeBeforeDissolve = rocketDAOProtocolSettingsMegapool.getTimeBeforeDissolve(); require(block.timestamp > validator.lastAssignmentTime + timeBeforeDissolve, "Not enough time has passed"); } // Apply a penalty by increasing debt uint256 dissolvePenalty = rocketDAOProtocolSettingsMegapool.getDissolvePenalty(); _increaseDebt(dissolvePenalty); // Update validator info validator.inPrestake = false; validator.dissolved = true; validator.lastAssignmentTime = 0; validators[_validatorId] = validator; // Decrease total bond used for bond requirement calculations uint256 capitalValue = uint256(validator.lastRequestedValue) * milliToWei; uint256 recycleValue = capitalValue - prestakeValue; (uint256 nodeShare, uint256 userShare) = _calculateCapitalDispersal(capitalValue, getActiveValidatorCount() - 1); nodeBond -= nodeShare; userCapital -= userShare; unchecked { // Infeasible overflow numInactiveValidators += 1; } // Snapshot capital ratio _snapshotCapitalRatio(); // Recycle ETH assignedValue -= recycleValue; // Calculate the values to send to node and user uint256 toUser = userShare; if (recycleValue < toUser) { uint256 shortFall = toUser - recycleValue; toUser -= shortFall; _increaseDebt(shortFall); } uint256 toNode = recycleValue - toUser; // Send funds RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool(); if (toUser > 0) { rocketDepositPool.recycleDissolvedDeposit{value: toUser}(); } rocketDepositPool.fundsReturned(nodeAddress, nodeShare, userShare); refundValue += toNode; // Emit event emit MegapoolValidatorDissolved(_validatorId, block.timestamp); } /// @notice Receives ETH, which is sent to the rETH contract, to repay a debt owed by the node operator function repayDebt() override external payable { require(msg.value > 0, "Invalid value received"); _repayDebt(msg.value); } /// @dev Internal implementation of the repay debt function /// @param _amount Amount of debt to repay function _repayDebt(uint256 _amount) internal { require(debt >= _amount, "Not enough debt"); _sendToRETH(_amount); _reduceDebt(_amount); } /// @notice Distributes any accrued staking rewards function distribute() override public { // Prevent calls before a megapool's first validator has been staked require(lastDistributionTime != 0, "No first validator"); // Distribute pending rewards _distributeAmount(getPendingRewards()); // If owner is calling, claim immediately if (isNodeCalling(msg.sender)) { _claim(); } } /// @dev Internal implementation of reward distribution process /// @param _rewards Amount of rewards to distribute function _distributeAmount(uint256 _rewards) internal { // Cannot distribute a megapool with exiting or locked validators require(numExitingValidators == 0, "Pending validator exit"); require(numLockedValidators == 0, "Megapool locked"); // Early out if there are no rewards to distribute if (_rewards == 0) { // Last distribution time still gets updated lastDistributionTime = block.timestamp; return; } (uint256 nodeAmount, uint256 voterAmount, uint256 protocolDAOAmount, uint256 rethAmount) = calculateRewards(_rewards); // Update last distribution time for use in calculating time-weighted average commission lastDistributionTime = block.timestamp; // Maybe repay debt from node share if (debt > 0) { uint256 amountToRepay = nodeAmount; if (amountToRepay > debt) { amountToRepay = debt; } nodeAmount -= amountToRepay; _repayDebt(amountToRepay); } // Send user share to rETH _sendToRETH(rethAmount); // Send voter share to rewards pool if (voterAmount > 0) { RocketRewardsPoolInterface rocketRewardsPool = RocketRewardsPoolInterface(getContractAddress("rocketRewardsPool")); rocketRewardsPool.depositVoterShare{value: voterAmount}(); } // Protocol DAO share to rocketClaimDAO if (protocolDAOAmount > 0) { address rocketClaimDAO = getContractAddress("rocketClaimDAO"); (bool success,) = rocketClaimDAO.call{value: protocolDAOAmount}(""); require(success, "Failed to send protocol DAO rewards"); } // Increase node rewards value refundValue += nodeAmount; // Emit event emit RewardsDistributed(nodeAmount, voterAmount, rethAmount, protocolDAOAmount, block.timestamp); } /// @notice Claims any distributed but unclaimed rewards function claim() override public onlyMegapoolOwner() { _claim(); } /// @dev Internal implementation of claim process function _claim() internal { uint256 amountToSend = refundValue; // If node operator has a debt, pay that off first if (debt > 0) { if (debt > amountToSend) { _repayDebt(amountToSend); amountToSend = 0; } else { amountToSend -= debt; _repayDebt(debt); } } // Zero out refund value refundValue = 0; // If there is still an amount to send after debt, do so now if (amountToSend > 0) { address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); (bool success,) = nodeWithdrawalAddress.call{value: amountToSend}(""); require(success, "Failed to send ETH"); } // Emit event emit RewardsClaimed(amountToSend, block.timestamp); } /// @notice Returns the calculated split of pending rewards function calculatePendingRewards() override external view returns (uint256 nodeRewards, uint256 voterRewards, uint256 protocolDAORewards, uint256 rethRewards) { return calculateRewards(getPendingRewards()); } /// @notice Calculates the split of rewards for a given amount of ETH /// @param _amount Amount of rewards in wei to calculate the split of function calculateRewards(uint256 _amount) public view returns (uint256 nodeRewards, uint256 voterRewards, uint256 protocolDAORewards, uint256 rethRewards) { // Early out for edge cases if (_amount == 0) return (0, 0, 0, 0); if (lastDistributionTime == 0) return (_amount, 0, 0, 0); // Calculate split based on capital ratio and average commission since last distribute RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); uint64 lastDistributionTime64 = SafeCast.toUint64(lastDistributionTime); (, uint256 voterShare, uint256 protocolDAOShare, uint256 rethShare) = rocketNetworkRevenues.calculateSplit(lastDistributionTime64); uint256 averageCapitalRatio = rocketNetworkRevenues.getNodeAverageCapitalRatioSince(nodeAddress, lastDistributionTime64); // Sanity check input values require(averageCapitalRatio <= calcBase, "Invalid average capital ratio"); require(voterShare + protocolDAOShare + rethShare <= calcBase, "Invalid shares"); unchecked { uint256 borrowedPortion = _amount - (_amount * averageCapitalRatio / calcBase ); rethRewards = rethShare * borrowedPortion / calcBase; voterRewards = voterShare * borrowedPortion / calcBase; protocolDAORewards = protocolDAOShare * borrowedPortion / calcBase; nodeRewards = _amount - rethRewards - voterRewards - protocolDAORewards; } } /// @notice Used to optimistically lock a megapool with an oDAO challenging that a validator has exited /// @param _validatorId Internal ID of the validator to lock function challengeExit(uint32 _validatorId) override external onlyRocketMegapoolManager { ValidatorInfo memory validator = validators[_validatorId]; // Check required state require(validator.staked, "Validator not staked"); require(!validator.exiting, "Already exiting"); require(!validator.exited, "Already exited"); // Only the first challenge increments the lock counter, subsequent challenges only update the lockedTime if (!validator.locked) { validator.locked = true; unchecked { // Infeasible overflow numLockedValidators += 1; } } // Update lockedTime to current time validator.lockedTime = SafeCast.toUint64(block.timestamp); validators[_validatorId] = validator; // Emit event emit MegapoolValidatorLocked(_validatorId, block.timestamp); } /// @notice Unlocks a challenged validator /// @param _validatorId Internal ID of the validator to lock /// @param _slotTimestamp Timestamp of the slot at which it was proved the validator is not exiting function notifyNotExit(uint32 _validatorId, uint64 _slotTimestamp) override external onlyRocketMegapoolManager { ValidatorInfo memory validator = validators[_validatorId]; // Check required state require(validator.locked, "Validator not locked"); require(_slotTimestamp >= validator.lockedTime, "Proof is older than challenge"); // Update validator state to unlocked validator.locked = false; validator.lockedTime = 0; // Decrement locked validator counter numLockedValidators -= 1; validators[_validatorId] = validator; // Emit event emit MegapoolValidatorUnlocked(_validatorId, block.timestamp); } /// @notice Used to notify the megapool that one of its validators is exiting the beaconchain /// @param _validatorId Internal ID of the validator to notify exit for /// @param _recentEpoch A recent epoch function notifyExit(uint32 _validatorId, uint64 _withdrawableEpoch, uint64 _recentEpoch) override external onlyRocketMegapoolManager { ValidatorInfo memory validator = validators[_validatorId]; // Check required state require(validator.staked || validator.dissolved, "Not staking or dissolved"); require(!validator.exiting, "Already notified"); require(!validator.exited, "Already exited"); // Update validator state to exiting validator.exiting = true; // Setup distribution lock unchecked { // Infeasible overflow numExitingValidators += 1; } // If validator was locked, notifying exit unlocks it if (validator.locked) { validator.locked = false; validator.lockedTime = 0; numLockedValidators -= 1; } validators[_validatorId] = validator; // Apply penalty for late submission RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); uint256 notifyThreshold = rocketDAOProtocolSettingsMegapool.getNotifyThreshold(); if (_recentEpoch > _withdrawableEpoch - notifyThreshold) { _increaseDebt(rocketDAOProtocolSettingsMegapool.getLateNotifyFine()); } // Emit event emit MegapoolValidatorExiting(_validatorId, block.timestamp); } /// @notice Used to notify the megapool of the final balance of an exited validator /// @param _validatorId Internal ID of the validator to notify final balance of /// @param _amountInGwei The amount in the final withdrawal /// @param _caller The address which is submitted the final balance (i.e. msg.sender passed from RocketMegapoolManager) /// @param _withdrawalEpoch The epoch containing the withdrawal /// @param _recentEpoch A recent epoch function notifyFinalBalance(uint32 _validatorId, uint64 _amountInGwei, address _caller, uint64 _withdrawalEpoch, uint64 _recentEpoch) override external onlyRocketMegapoolManager { // Perform notification process bool incursShortfall = _notifyFinalBalance(_validatorId, _amountInGwei); // Trigger a deposit of excess collateral from rETH contract to deposit pool RocketTokenRETHInterface(rocketTokenRETH).depositExcessCollateral(); // If owner is calling, claim immediately if (isNodeCalling(_caller)) { _claim(); } else { // Permissionless distribute requires a wait time depending on if the final balance results in a shortfall of user funds RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); if (incursShortfall) { uint256 userDistributeDelay = rocketDAOProtocolSettingsMegapool.getUserDistributeDelayWithShortfall(); require(uint256(_recentEpoch) >= _withdrawalEpoch + userDistributeDelay, "Not enough time has passed"); } else { uint256 userDistributeDelay = rocketDAOProtocolSettingsMegapool.getUserDistributeDelay(); require(uint256(_recentEpoch) >= _withdrawalEpoch + userDistributeDelay, "Not enough time has passed"); } } } /// @dev Internal implementation of final balance notification process /// @param _validatorId Internal ID of the validator to notify final balance of /// @param _amountInGwei The amount in the final withdrawal /// @return Returns true if the final balance results in a shortfall of user capital function _notifyFinalBalance(uint32 _validatorId, uint64 _amountInGwei) internal returns (bool) { ValidatorInfo memory validator = validators[_validatorId]; require(!validator.exited, "Already exited"); require(validator.exiting, "Validator not exiting"); bool incursShortfall = false; // Mark as exited validator.exited = true; validator.exiting = false; validator.exitBalance = _amountInGwei; uint256 withdrawalBalance = uint256(_amountInGwei) * 1 gwei; validators[_validatorId] = validator; if (!validator.dissolved) { // Calculate capital distribution amounts uint256 depositBalance = uint256(validator.depositValue) * milliToWei; (uint256 nodeShare, uint256 userShare) = _calculateCapitalDispersal(depositBalance, getActiveValidatorCount() - 1); { uint256 toNode = nodeShare; if (withdrawalBalance < depositBalance) { uint256 shortfall = depositBalance - withdrawalBalance; if (shortfall > toNode) { toNode = 0; } else { toNode -= shortfall; } } uint256 toUser = withdrawalBalance - toNode; // Pay off any existing debt and any new debt introduced by this exit if (toUser < userShare) { _increaseDebt(userShare - toUser); incursShortfall = true; } if (toNode > 0 && debt > 0) { if (toNode > debt) { toNode -= debt; toUser += debt; _reduceDebt(debt); } else { toUser += toNode; _reduceDebt(toNode); toNode = 0; } } // Send funds _sendToRETH(toUser); if (toNode > 0) { refundValue += toNode; } } // Update state if (nodeShare > 0) { nodeBond -= nodeShare; } if (userShare > 0) { userCapital -= userShare; } unchecked { // Infeasible overflow numInactiveValidators += 1; } // Handle collateral change RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool(); rocketDepositPool.fundsReturned(nodeAddress, nodeShare, userShare); } // Remove distribution lock numExitingValidators -= 1; // Snapshot capital ratio _snapshotCapitalRatio(); // Emit event emit MegapoolValidatorExited(_validatorId, block.timestamp); // Return true if final balance results in a shortfall return incursShortfall; } /// @notice Applies a penalty via increase debt (only callable from rocketMegapoolPenalties) /// @param _amount Amount of the penalty function applyPenalty(uint256 _amount) override external onlyLatestContract("rocketMegapoolPenalties", msg.sender) { _increaseDebt(_amount); emit MegapoolPenaltyApplied(_amount, block.timestamp); } /// @dev Increases debt of this megapool function _increaseDebt(uint256 _amount) internal { debt += _amount; emit MegapoolDebtIncreased(_amount, block.timestamp); } /// @dev Reduces debt of this megapool function _reduceDebt(uint256 _amount) internal { debt -= _amount; emit MegapoolDebtReduced(_amount, block.timestamp); } /// @dev Helper function to send an amount of ETH to the RETH token conract function _sendToRETH(uint256 _amount) internal { if (_amount == 0) { return; } (bool success,) = rocketTokenRETH.call{value: _amount}(""); require(success); } /// @dev Calculates share of returned capital based on current bond level and requirement /// @param _value The amount of ETH capital that is needing to be dispersed /// @param _newValidatorCount The number of validators the node will have after this dispersal function _calculateCapitalDispersal(uint256 _value, uint256 _newValidatorCount) internal view returns (uint256 _nodeShare, uint256 _userShare) { RocketNodeDepositInterface rocketNodeDeposit = _getRocketNodeDeposit(); uint256 newBondRequirement = rocketNodeDeposit.getBondRequirement(_newValidatorCount); uint256 effectiveBond = nodeBond + nodeQueuedBond; _nodeShare = 0; if (newBondRequirement < effectiveBond) { _nodeShare = effectiveBond - newBondRequirement; } if (_nodeShare > _value) { _nodeShare = _value; } if (_nodeShare > nodeBond) { _nodeShare = nodeBond; } _userShare = _value - _nodeShare; } /// @dev Convenience function to return interface to RocketDepositPool function _getRocketDepositPool() internal view returns (RocketDepositPoolInterface) { return RocketDepositPoolInterface(rocketStorage.getAddress(rocketDepositPoolKey)); } /// @dev Convenience function to return interface to RocketNodeDeposit function _getRocketNodeDeposit() internal view returns (RocketNodeDepositInterface) { return RocketNodeDepositInterface(rocketStorage.getAddress(rocketNodeDepositKey)); } /// @dev Attempts to distribute rewards at current ratio before snapshotting capital ratio function _snapshotCapitalRatio() internal { // Try to distribute rewards before updating capital ratio if (numExitingValidators == 0 && numLockedValidators == 0) { _distributeAmount(getPendingRewards()); } // Snapshot capital ratio _calculateAndSaveCapitalRatio(); } /// @dev Calculates the current capital ratio of this Megapool and notifies RocketNetworkRevenues to snapshot it function _calculateAndSaveCapitalRatio() internal { // Calculate and send capital ratio to RocketNetworkRevenues for snapshotting RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); if (nodeBond + userCapital > 0) { // Stored as a ratio of node bond / total capital uint256 capitalRatio = nodeBond * calcBase / (userCapital + nodeBond); rocketNetworkRevenues.setNodeCapitalRatio(nodeAddress, capitalRatio); } } /// @dev Mirror deposit contract deposit data root calculation but with in-memory bytes instead of calldata function _computeDepositDataRoot(bytes memory pubkey, bytes memory signature, uint64 amount) internal view returns (bytes32 ret) { bytes32 withdrawalCredentials = getWithdrawalCredentials(); assembly { let result let temp := mload(0x40) // [0x00] = pubkey[0x00:0x20] // [0x20] = pubkey[0x20:0x30] . bytes16(0) mstore(0x00, mload(add(pubkey, 0x20))) mstore(0x20, and(mload(add(pubkey, 0x40)), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000)) // temp[0x00] = sha256([0x00:0x40]) result := staticcall(gas(), 0x02, 0x00, 0x40, temp, 0x20) if iszero(result) { revert(0, 0) } // temp[0x20] = withdrawal_credentials mstore(add(temp, 0x20), withdrawalCredentials) // temp[0x00] = sha256(temp[0x00:0x40]) result := staticcall(gas(), 0x02, temp, 0x40, temp, 0x20) if iszero(result) { revert(0, 0) } // temp[0x20] = sha256(signature[0x00:0x40]) result := staticcall(gas(), 0x02, add(signature, 0x20), 0x40, add(temp, 0x20), 0x20) if iszero(result) { revert(0, 0) } // [0x00] = signature[0x40] // [0x20] = bytes32(0) mstore(0x00, mload(add(signature, 0x60))) mstore(0x20, 0) // [0x20] = sha256([0x00:0x40]) result := staticcall(gas(), 0x02, 0x00, 0x40, 0x20, 0x20) if iszero(result) { revert(0, 0) } // [0x00] = temp[0x20] mstore(0x00, mload(add(temp, 0x20))) // [0x20] = sha256([0x00:0x40]) result := staticcall(gas(), 0x02, 0x00, 0x40, 0x20, 0x20) if iszero(result) { revert(0, 0) } // [0x00] = to_little_endian(amount) . bytes24(0) mstore(0x00, 0) mstore8(0x00, shr(0x00, amount)) mstore8(0x01, shr(0x08, amount)) mstore8(0x02, shr(0x10, amount)) mstore8(0x03, shr(0x18, amount)) mstore8(0x04, shr(0x20, amount)) mstore8(0x05, shr(0x28, amount)) mstore8(0x06, shr(0x30, amount)) mstore8(0x07, shr(0x38, amount)) // [0x20] = sha256([0x00:0x40]) result := staticcall(gas(), 0x02, 0x00, 0x40, 0x20, 0x20) if iszero(result) { revert(0, 0) } // [0x00] = temp[0x00] mstore(0x00, mload(temp)) // [0x00] = sha256([0x00:0x40]) result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) if iszero(result) { revert(0, 0) } // Return [0x00:0x20] ret := mload(0x00) } } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolDelegateBase.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketBase} from "../RocketBase.sol"; import {RocketMegapoolDelegateBaseInterface} from "../../interface/megapool/RocketMegapoolDelegateBaseInterface.sol"; import {RocketMegapoolStorageLayout} from "./RocketMegapoolStorageLayout.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; /// @dev All megapool delegate contracts must extend this base to include the expected deprecation functionality contract RocketMegapoolDelegateBase is RocketMegapoolStorageLayout, RocketMegapoolDelegateBaseInterface { // Constants uint256 constant internal upgradeBuffer = 120 days; // Immutables RocketStorageInterface immutable internal rocketStorage; uint256 immutable public version; constructor(RocketStorageInterface _rocketStorageAddress, uint256 _version) { version = _version; rocketStorage = _rocketStorageAddress; } /// @notice Called by an upgrade to begin the expiry countdown for this delegate /// @dev The expiration block can only ever be set to an offset from the current block to prevent malicious oDAO /// from manually expiring a delegate and forcing node operators onto a new one without a delay function deprecate() external override onlyLatestNetworkContract { // Expiry is only used on the delegate contract itself require(!storageState); expirationTime = block.timestamp + upgradeBuffer; } /// @notice Returns the time at which this delegate expires (or 0 if not yet deprecated) function getExpirationTime() external override view returns (uint256) { return expirationTime; } // // Internals // /// @dev Get the address of a Rocket Pool network contract /// @param _contractName The internal name of the contract to retrieve the address for function getContractAddress(string memory _contractName) internal view returns (address) { address contractAddress = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); require(contractAddress != address(0x0), "Contract not found"); return contractAddress; } // // Modifiers // /// @dev Reverts if caller is not the owner of the megapool modifier onlyMegapoolOwner() { require(isNodeCalling(msg.sender), "Not allowed"); _; } /// @dev Reverts if called by any sender that doesn't match one of the supplied contract or is the latest version of that contract modifier onlyLatestContract(string memory _contractName, address _contractAddress) { require(_contractAddress == rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", _contractName))), "Invalid or outdated contract"); _; } /// @dev Reverts if not called by a valid network contract modifier onlyLatestNetworkContract() { require(rocketStorage.getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))), "Invalid or outdated network contract"); _; } /// @dev Returns true if msg.sender is node or node's withdrawal address function isNodeCalling(address _caller) internal view returns (bool) { if (_caller == nodeAddress) { return true; } else { address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); if (_caller == withdrawalAddress) { return true; } } return false; } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolFactory.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketMegapoolDelegateBaseInterface} from "../../interface/megapool/RocketMegapoolDelegateBaseInterface.sol"; import {RocketMegapoolFactoryInterface} from "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; import {RocketMegapoolProxyInterface} from "../../interface/megapool/RocketMegapoolProxyInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {Clones} from "@openzeppelin4/contracts/proxy/Clones.sol"; /// @notice Performs deterministic deployment of megapool delegate contracts and handles deprecation of old ones contract RocketMegapoolFactory is RocketBase, RocketMegapoolFactoryInterface { // Immutables uint256 private immutable setKey; // Libs using Clones for address; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; // Initialise immutables setKey = uint256(keccak256(abi.encodePacked("megapool.delegate.set"))); } /// @notice Used following an upgrade or new deployment to initialise the delegate list function initialise() override public { // On new deploy, allow guardian to initialise, otherwise, only a network contract if (rocketStorage.getDeployedStatus()) { require(getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))), "Invalid or outdated network contract"); } else { require(msg.sender == rocketStorage.getGuardian(), "Not guardian"); } // Prevent multiple initialisations by checking if the meta struct is non-zero bytes32 metaKey = bytes32(setKey); uint256 meta = getUint(metaKey); require(meta == 0, "Already initialised"); // Initialise the delegate list _upgradeDelegate(getContractAddress("rocketMegapoolDelegate")); } /// @notice Returns the expected megapool address for a node operator /// @param _nodeAddress Address of the node operator to compute the megapool address for function getExpectedAddress(address _nodeAddress) override public view returns (address) { // Ensure rocketMegapoolBase is setAddress address rocketMegapoolProxy = getContractAddress("rocketMegapoolProxy"); // Calculate node specific salt value bytes32 salt = keccak256(abi.encodePacked(_nodeAddress)); // Return expected address return rocketMegapoolProxy.predictDeterministicAddress(salt, address(this)); } /// @notice Returns true if the given node operator has deployed their megapool /// @param _nodeAddress Address of the node operator to query function getMegapoolDeployed(address _nodeAddress) override external view returns (bool) { address contractAddress = getExpectedAddress(_nodeAddress); return getBool(keccak256(abi.encodePacked("megapool.exists", contractAddress))); } /// @notice Deploys a megapool for the given node operator /// @param _nodeAddress Owning node operator's address function deployContract(address _nodeAddress) override public onlyLatestContract("rocketNodeManager", msg.sender) onlyLatestContract("rocketMegapoolFactory", address(this)) returns (address) { return _deployContract(_nodeAddress); } /// @dev Deploys a megapool contract for the given node operator function _deployContract(address _nodeAddress) internal returns (address) { // Ensure rocketMegapoolBase is setAddress address rocketMegapoolProxy = getContractAddress("rocketMegapoolProxy"); require(rocketMegapoolProxy != address(0), "Invalid proxy"); // Check if already deployed require(!getBool(keccak256(abi.encodePacked("megapool.exists", getExpectedAddress(_nodeAddress)))), "Megapool already deployed for node operator"); // Construct final salt bytes32 salt = keccak256(abi.encodePacked(_nodeAddress)); // Deploy the megapool address proxy = rocketMegapoolProxy.cloneDeterministic(salt); // Initialise the megapool storage RocketMegapoolProxyInterface(proxy).initialise(_nodeAddress); // Mark as valid megapool address setBool(keccak256(abi.encodePacked("megapool.exists", proxy)), true); // Return address return proxy; } /// @notice Returns megapool address for given node, deploys if it doesn't exist yet /// @param _nodeAddress Owning node operator's address function getOrDeployContract(address _nodeAddress) override external onlyLatestContract("rocketNodeDeposit", msg.sender) onlyLatestContract("rocketMegapoolFactory", address(this)) returns (address) { address contractAddress = getExpectedAddress(_nodeAddress); if (getBool(keccak256(abi.encodePacked("megapool.exists", contractAddress)))) { return contractAddress; } return _deployContract(_nodeAddress); } /// @notice Returns the expiration time of the given delegate (or 0 if not deprecated yet) /// @param _delegateAddress Address of the delegate to query function getDelegateExpiry(address _delegateAddress) override external view returns (uint256) { RocketMegapoolDelegateBaseInterface deprecatedDelegate = RocketMegapoolDelegateBaseInterface(_delegateAddress); return deprecatedDelegate.getExpirationTime(); } /// @notice Called during an upgrade to publish a new delegate and deprecate any in-use ones /// @param _newDelegateAddress The address of the new delegate to upgrade to function upgradeDelegate(address _newDelegateAddress) override public onlyLatestContract("rocketMegapoolFactory", address(this)) onlyLatestNetworkContract() { _upgradeDelegate(_newDelegateAddress); } /// @dev Performs the deprecation of old delegates and insertion of the new one into the dequeue /// @param _newDelegateAddress The address of the new delegate to upgrade to function _upgradeDelegate(address _newDelegateAddress) internal { /* A set of all past delegates is stored in RocketStorage as a dequeue with the following layout: Uint storage: keccak("megapool.delegate.set") : struct Metadata { uint128 tail, uint128 head } Address storage: keccak("megapool.delegate.set") + 0: delegate 0 " + 1: delegate 1 <-- head (oldest unexpired delegate) " + 2: delegate 2 " + 3: delegate 3 (latest delegate) " + 4: empty <-- tail */ // Compute storage keys bytes32 metaKey = bytes32(setKey); // Retrieve set metadata uint256 meta = getUint(metaKey); uint128 head = uint128(meta); uint128 tail = uint128(meta >> 128); // Expiry times should be sequential, but just in case we'll only advance the head if none before it have expired bool deprecatedOne = false; // Iterate over "in-use" delegates and deprecate them if they are yet to expire for (uint256 i = head; i < tail; ++i) { RocketMegapoolDelegateBaseInterface delegate = RocketMegapoolDelegateBaseInterface(getAddress(bytes32(setKey + i))); uint256 expiry = delegate.getExpirationTime(); if (expiry == 0 || block.timestamp < expiry) { // This delegate is still "in-use" so set the expiry block into the future delegate.deprecate(); deprecatedOne = true; } else if (!deprecatedOne) { // This delegate has already expired so no longer considered "in-use", advance the head head += 1; } } // Push new delegate address to set setAddress(bytes32(setKey + tail), _newDelegateAddress); tail += 1; // Update meta meta = (uint256(tail) << 128) | uint256(head); setUint(metaKey, meta); // Set the current delegate setAddress(keccak256(abi.encodePacked("contract.address", "rocketMegapoolDelegate")), _newDelegateAddress); } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolManager.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketMegapoolStorageLayout} from "./RocketMegapoolStorageLayout.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketMegapoolInterface} from "../../interface/megapool/RocketMegapoolInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketMegapoolManagerInterface} from "../../interface/megapool/RocketMegapoolManagerInterface.sol"; import {BeaconStateVerifierInterface, ValidatorProof, Withdrawal, WithdrawalProof, SlotProof} from "../../interface/util/BeaconStateVerifierInterface.sol"; /// @notice Handles protocol-level megapool functionality contract RocketMegapoolManager is RocketBase, RocketMegapoolManagerInterface { // Immutables bytes32 immutable internal challengerKey; bytes32 immutable internal setCountKey; // Constants uint256 constant internal farFutureEpoch = 2 ** 64 - 1; uint256 constant internal activationBalanceInGwei = 32 ether / 1 gwei; uint64 constant internal slotsPerEpoch = 32; uint256 constant internal slotRecencyThreshold = 1 hours; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; // Precompute static storage keys challengerKey = keccak256("last.trusted.node.megapool.challenger"); setCountKey = keccak256("megapool.validator.set.count"); } /// @notice Returns the total number validators across all megapools function getValidatorCount() override external view returns (uint256) { return getUint(setCountKey); } /// @notice Adds a validator record to the global megapool validator set /// @param _megapoolAddress Address of the megapool which manages this validator /// @param _validatorId Internal validator ID of the new validator function addValidator(address _megapoolAddress, uint32 _validatorId, bytes calldata _pubkey) override external onlyLatestContract("rocketMegapoolManager", address(this)) onlyLatestContract("rocketNodeDeposit", msg.sender) { uint256 index = getUint(setCountKey); setUint(setCountKey, index + 1); uint256 encoded = (uint256(uint160(_megapoolAddress)) << 96) | _validatorId; setUint(keccak256(abi.encodePacked("megapool.validator.set", index)), encoded); // Add pubkey => megapool mapping and ensure uniqueness bytes32 key = keccak256(abi.encodePacked("validator.megapool", _megapoolAddress, _pubkey)); require(getAddress(key) == address(0x0), "Pubkey in use"); setAddress(key, _megapoolAddress); } /// @notice Returns the last trusted member to execute a challenge function getLastChallenger() override external view returns (address) { return getAddress(challengerKey); } /// @notice Returns validator info for the given global megapool validator index /// @param _index The index of the validator to query function getValidatorInfo(uint256 _index) override external view returns (bytes memory pubkey, RocketMegapoolStorageLayout.ValidatorInfo memory validatorInfo, address megapool, uint32 validatorId) { // Retrieve and decode entry uint256 encoded = getUint(keccak256(abi.encodePacked("megapool.validator.set", _index))); megapool = address(uint160(encoded >> 96)); validatorId = uint32(encoded); // Fetch and return info RocketMegapoolInterface rocketMegapool = RocketMegapoolInterface(megapool); (validatorInfo, pubkey) = rocketMegapool.getValidatorInfoAndPubkey(validatorId); } /// @notice Verifies a validator state proof then calls stake on the megapool /// @param _megapool Address of the megapool which the validator belongs to /// @param _validatorId Internal ID of the validator within the megapool /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _validatorProof State proof of the validator /// @param _slotProof State proof of the slot function stake(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) override external onlyRegisteredMegapool(address(_megapool)) { // Require a recent proof require(_slotTimestamp + slotRecencyThreshold >= block.timestamp, "Slot proof too old"); // Verify state proofs BeaconStateVerifierInterface beaconStateVerifier = BeaconStateVerifierInterface(getContractAddress("beaconStateVerifier")); require(beaconStateVerifier.verifyValidator(_slotTimestamp, _slotProof.slot, _validatorProof), "Invalid validator proof"); require(beaconStateVerifier.verifySlot(_slotTimestamp, _slotProof), "Invalid slot proof"); bytes32 withdrawalCredentials = _megapool.getWithdrawalCredentials(); // Verify validator state require(_validatorProof.validator.withdrawalCredentials == withdrawalCredentials, "Invalid withdrawal credentials"); require(_validatorProof.validator.withdrawableEpoch == farFutureEpoch, "Validator is withdrawing"); require(_validatorProof.validator.exitEpoch == farFutureEpoch, "Validator is exiting"); require(_validatorProof.validator.effectiveBalance < activationBalanceInGwei, "Invalid validator balance"); require(_validatorProof.validator.activationEligibilityEpoch == farFutureEpoch, "Validator is activating"); require(_validatorProof.validator.activationEpoch == farFutureEpoch, "Validator is activated"); require(!_validatorProof.validator.slashed, "Validator is slashed"); // Verify matching pubkey bytes memory pubkey = _megapool.getValidatorPubkey(_validatorId); require(keccak256(_validatorProof.validator.pubkey) == keccak256(pubkey), "Pubkey does not match"); // Perform the stake _megapool.stake(_validatorId); } /// @notice Immediately dissolves a validator if any validator state is non-compliant /// @param _megapool Address of the megapool which the validator belongs to /// @param _validatorId Internal ID of the validator within the megapool /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _validatorProof State proof of the validator /// @param _slotProof State proof of the slot function dissolve(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) override external onlyRegisteredMegapool(address(_megapool)) { // Require a recent proof require(_slotTimestamp + slotRecencyThreshold >= block.timestamp, "Slot proof too old"); // Verify state proofs BeaconStateVerifierInterface beaconStateVerifier = BeaconStateVerifierInterface(getContractAddress("beaconStateVerifier")); require(beaconStateVerifier.verifyValidator(_slotTimestamp, _slotProof.slot, _validatorProof), "Invalid validator proof"); require(beaconStateVerifier.verifySlot(_slotTimestamp, _slotProof), "Invalid slot proof"); // Verify compliant validator state bytes32 withdrawalCredentials = _megapool.getWithdrawalCredentials(); if( _validatorProof.validator.withdrawalCredentials == withdrawalCredentials && _validatorProof.validator.withdrawableEpoch == farFutureEpoch && _validatorProof.validator.exitEpoch == farFutureEpoch && _validatorProof.validator.effectiveBalance < activationBalanceInGwei && _validatorProof.validator.activationEligibilityEpoch == farFutureEpoch && _validatorProof.validator.activationEpoch == farFutureEpoch && _validatorProof.validator.slashed == false ) { revert("Validator is compliant"); } // Verify matching pubkey bytes memory pubkey = _megapool.getValidatorPubkey(_validatorId); require(keccak256(_validatorProof.validator.pubkey) == keccak256(pubkey), "Pubkey does not match"); // Dissolve the validator _megapool.dissolveValidator(_validatorId); } /// @notice Verifies a validator state proof then notifies megapool about the exit /// @param _megapool Address of the megapool which the validator belongs to /// @param _validatorId Internal ID of the validator within the megapool /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _validatorProof State proof of the validator /// @param _slotProof State proof of the slot function notifyExit(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) override external onlyRegisteredMegapool(address(_megapool)) { // Require a recent proof require(_slotTimestamp + slotRecencyThreshold >= block.timestamp, "Slot proof too old"); // Verify state proofs BeaconStateVerifierInterface beaconStateVerifier = BeaconStateVerifierInterface(getContractAddress("beaconStateVerifier")); require(beaconStateVerifier.verifyValidator(_slotTimestamp, _slotProof.slot, _validatorProof), "Invalid validator proof"); require(beaconStateVerifier.verifySlot(_slotTimestamp, _slotProof), "Invalid slot proof"); // Verify correct withdrawable_epoch require(_validatorProof.validator.withdrawableEpoch < farFutureEpoch, "Validator not exiting"); // Verify matching pubkey bytes memory pubkey = _megapool.getValidatorPubkey(_validatorId); require(keccak256(_validatorProof.validator.pubkey) == keccak256(pubkey), "Pubkey does not match"); // Verify withdrawalCredentials bytes32 withdrawalCredentials = _megapool.getWithdrawalCredentials(); require(_validatorProof.validator.withdrawalCredentials == withdrawalCredentials, "Invalid withdrawal credentials"); // Compute the epoch of the supplied proof uint64 recentEpoch = _slotProof.slot / slotsPerEpoch; // Notify megapool _megapool.notifyExit(_validatorId, _validatorProof.validator.withdrawableEpoch, recentEpoch); } /// @notice Verifies a validator state proof then notifies megapool that this validator was not exiting at given slot /// @param _megapool Address of the megapool which the validator belongs to /// @param _validatorId Internal ID of the validator within the megapool /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _validatorProof State proof of the validator /// @param _slotProof State proof of the slot function notifyNotExit(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) override external onlyRegisteredMegapool(address(_megapool)) { // Require a recent proof require(_slotTimestamp + slotRecencyThreshold >= block.timestamp, "Slot proof too old"); // Verify state proofs BeaconStateVerifierInterface beaconStateVerifier = BeaconStateVerifierInterface(getContractAddress("beaconStateVerifier")); require(beaconStateVerifier.verifyValidator(_slotTimestamp, _slotProof.slot, _validatorProof), "Invalid validator proof"); require(beaconStateVerifier.verifySlot(_slotTimestamp, _slotProof), "Invalid slot proof"); // Verify correct withdrawable_epoch require(_validatorProof.validator.withdrawableEpoch == farFutureEpoch, "Validator already exiting"); // Verify matching pubkey bytes memory pubkey = _megapool.getValidatorPubkey(_validatorId); require(keccak256(_validatorProof.validator.pubkey) == keccak256(pubkey), "Pubkey does not match"); // Notify the megapool that the specified validator was not exiting at the proven slot _megapool.notifyNotExit(_validatorId, _slotTimestamp); } /// @notice Asserts that one or more megapool validators are exiting but a proof has not been supplied by the node operator /// @param _challenges List of challenges to submit /// @dev Only a trusted node can submit challenges function challengeExit(ExitChallenge[] calldata _challenges) override external onlyTrustedNode(msg.sender) { // Check if this member was the previous one to challenge address lastSubmitter = getAddress(challengerKey); require(msg.sender != lastSubmitter, "Member was last to challenge"); setAddress(challengerKey, msg.sender); // Deliver challenges uint256 totalChallenges = 0; for (uint256 i = 0; i < _challenges.length; ++i) { for (uint256 j = 0; j < _challenges[i].validatorIds.length; ++j) { require(getBool(keccak256(abi.encodePacked("megapool.exists", address(_challenges[i].megapool)))), "Invalid megapool"); _challenges[i].megapool.challengeExit(_challenges[i].validatorIds[j]); totalChallenges += 1; } } // Only allow up to 50 total challenges at a time require(totalChallenges <= 50, "Too many challenges"); } /// @notice Verifies a withdrawal state proof then notifies megapool of the final balance /// @param _megapool Address of the megapool which the validator belongs to /// @param _validatorId Internal ID of the validator within the megapool /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _withdrawalProof State proof of the withdrawal /// @param _validatorProof State proof of the validator at the same slot as the withdrawal /// @param _slotProof State proof of the slot function notifyFinalBalance(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, WithdrawalProof calldata _withdrawalProof, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) override external onlyRegisteredMegapool(address(_megapool)) { // Require a recent proof require(_slotTimestamp + slotRecencyThreshold >= block.timestamp, "Slot proof too old"); // Check that the withdrawal occurred on or after `withdrawable_epoch` uint64 withdrawalEpoch = _withdrawalProof.withdrawalSlot / slotsPerEpoch; require(withdrawalEpoch >= _validatorProof.validator.withdrawableEpoch, "Not full withdrawal"); // Verify state proofs BeaconStateVerifierInterface beaconStateVerifier = BeaconStateVerifierInterface(getContractAddress("beaconStateVerifier")); require(beaconStateVerifier.verifyValidator(_slotTimestamp, _slotProof.slot, _validatorProof), "Invalid validator proof"); require(beaconStateVerifier.verifyWithdrawal(_slotTimestamp, _slotProof.slot, _withdrawalProof), "Invalid withdrawal proof"); require(beaconStateVerifier.verifySlot(_slotTimestamp, _slotProof), "Invalid slot proof"); // Verify withdrawal validator index matches validator index require(_withdrawalProof.withdrawal.validatorIndex == _validatorProof.validatorIndex, "Withdrawal validator not matching"); // Verify matching pubkey bytes memory pubkey = _megapool.getValidatorPubkey(_validatorId); require(keccak256(_validatorProof.validator.pubkey) == keccak256(pubkey), "Pubkey does not match"); // Compute the epoch of the supplied proof uint64 recentEpoch = _slotProof.slot / slotsPerEpoch; // Notify megapool _megapool.notifyFinalBalance(_validatorId, _withdrawalProof.withdrawal.amountInGwei, msg.sender, withdrawalEpoch, recentEpoch); } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolPenalties.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketDAONodeTrustedInterface} from "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import {RocketDAOProtocolSettingsMegapoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMegapoolInterface.sol"; import {RocketMegapoolDelegateInterface} from "../../interface/megapool/RocketMegapoolDelegateInterface.sol"; import {RocketMegapoolPenaltiesInterface} from "../../interface/megapool/RocketMegapoolPenaltiesInterface.sol"; import {RocketNetworkSnapshotsTimeInterface} from "../../interface/network/RocketNetworkSnapshotsTimeInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; /// @notice Applies penalties to megapools for MEV theft contract RocketMegapoolPenalties is RocketBase, RocketMegapoolPenaltiesInterface { // Constants uint256 constant internal penaltyMaximumPeriod = 7 days; bytes32 constant internal penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty")); // Events event PenaltySubmitted(address indexed from, address megapool, uint256 block, uint256 amount, uint256 time); event PenaltyApplied(address indexed megapool, uint256 block, uint256 amount, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @notice Returns the number of votes in favour of the given penalty /// @param _megapool Address of the accused megapool /// @param _block Block that the theft occurred (used for uniqueness) /// @param _amount Amount in ETH of the penalty function getVoteCount(address _megapool, uint256 _block, uint256 _amount) override external view returns (uint256) { bytes32 submissionCountKey = keccak256(abi.encodePacked("megapool.penalty.submission", _megapool, _block, _amount)); return getUint(submissionCountKey); } /// @notice Votes to penalise a megapool for MEV theft (only callable by oDAO) /// @param _megapool Address of the accused megapool /// @param _block Block that the theft occurred (used for uniqueness) /// @param _amount Amount in ETH of the penalty function penalise(address _megapool, uint256 _block, uint256 _amount) override external onlyTrustedNode(msg.sender) onlyRegisteredMegapool(_megapool) { require(_amount > 0, "Invalid penalty amount"); require(_block < block.number, "Invalid block number"); // Sanity check amount does not exceed max penalty RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); uint256 maxPenalty = rocketDAOProtocolSettingsMegapool.getMaximumEthPenalty(); require(_amount <= maxPenalty, "Penalty exceeds maximum"); // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("megapool.penalty.submission", msg.sender, _megapool, _block, _amount)); bytes32 submissionCountKey = keccak256(abi.encodePacked("megapool.penalty.submission", _megapool, _block, _amount)); // Check & update node submission status require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); // Maybe execute _maybeApplyPenalty(_megapool, _block, _amount, submissionCount); // Emit event emit PenaltySubmitted(msg.sender, _megapool, _block, _amount, block.timestamp); } /// @notice Manually execute a penalty that has hit majority vote /// @param _megapool Address of the accused megapool /// @param _block Block that the theft occurred (used for uniqueness) /// @param _amount Amount in ETH of the penalty function executePenalty(address _megapool, uint256 _block, uint256 _amount) override external { // Get submission count bytes32 submissionCountKey = keccak256(abi.encodePacked("megapool.penalty.submission", _megapool, _block, _amount)); uint256 submissionCount = getUint(submissionCountKey); // Apply penalty if relevant conditions are met _maybeApplyPenalty(_megapool, _block, _amount, submissionCount); } /// @notice Returns the running total of penalties at a given time /// @param _time The time to compute running total for function getPenaltyRunningTotalAtTime(uint64 _time) override external view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshots = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); return rocketNetworkSnapshots.lookup(penaltyKey, _time); } /// @notice Returns the running total of penalties at the current block function getCurrentPenaltyRunningTotal() override external view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); (,,uint192 value) = rocketNetworkSnapshotsTime.latest(penaltyKey); return uint256(value); } /// @notice Returns the current maximum penalty based on the running total limitation function getCurrentMaxPenalty() override external view returns (uint256) { // Get contracts RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); // Grab max weekly penalty uint256 maxPenalty = rocketDAOProtocolSettingsMegapool.getMaximumEthPenalty(); // Get running total from 7 days ago uint256 earlierTime = 0; if (block.timestamp > penaltyMaximumPeriod) { earlierTime = block.timestamp - penaltyMaximumPeriod; } uint256 earlierRunningTotal = uint256(rocketNetworkSnapshotsTime.lookup(penaltyKey, SafeCast.toUint64(earlierTime))); // Get current running total (,, uint192 currentRunningTotal) = rocketNetworkSnapshotsTime.latest(penaltyKey); // Cap the penalty at the maximum amount based on past 7 days uint256 currentTotal = uint256(currentRunningTotal) - earlierRunningTotal; if (currentTotal > maxPenalty) return 0; return maxPenalty - currentTotal; } /// @dev If a penalty has not been applied and hit majority, execute the penalty /// @param _megapool Address of the accused megapool /// @param _block Block that the theft occurred (used for uniqueness) /// @param _amount Amount in ETH of the penalty function _maybeApplyPenalty(address _megapool, uint256 _block, uint256 _amount, uint256 _submissionCount) internal { // Check this penalty hasn't already reach majority and been applied bytes32 penaltyAppliedKey = keccak256(abi.encodePacked("megapool.penalty.submission.applied", _megapool, _block, _amount)); require(!getBool(penaltyAppliedKey), "Penalty already applied"); // Check for majority RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); if (calcBase * _submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsMegapool.getPenaltyThreshold()) { // Apply penalty and mark as applied _applyPenalty(_megapool, _amount); setBool(penaltyAppliedKey, true); // Emit event emit PenaltyApplied(_megapool, _block, _amount, block.timestamp); } } /// @dev Applies a penalty up to given amount, honouring the max penalty parameter function _applyPenalty(address _megapool, uint256 _amount) internal { // Get contracts RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool")); // Grab max weekly penalty uint256 maxPenalty = rocketDAOProtocolSettingsMegapool.getMaximumEthPenalty(); // Get running total from 7 days ago uint256 earlierTime = 0; if (block.timestamp > penaltyMaximumPeriod) { earlierTime = block.timestamp - penaltyMaximumPeriod; } uint256 earlierRunningTotal = rocketNetworkSnapshotsTime.lookup(penaltyKey, SafeCast.toUint64(earlierTime)); // Get current running total (,, uint192 currentRunningTotal) = rocketNetworkSnapshotsTime.latest(penaltyKey); // Prevent the running penalty total from exceeding the maximum amount uint256 currentTotal = uint256(currentRunningTotal) - earlierRunningTotal; require(currentTotal < maxPenalty, "Max penalty exceeded"); uint256 currentMaxPenalty = maxPenalty - currentTotal; require(_amount <= currentMaxPenalty, "Max penalty exceeded"); // Insert new running total rocketNetworkSnapshotsTime.push(penaltyKey, currentRunningTotal + SafeCast.toUint192(_amount)); // Call megapool to increase debt RocketMegapoolDelegateInterface(_megapool).applyPenalty(_amount); } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolProxy.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketMegapoolDelegateBaseInterface} from "../../interface/megapool/RocketMegapoolDelegateBaseInterface.sol"; import {RocketMegapoolProxyInterface} from "../../interface/megapool/RocketMegapoolProxyInterface.sol"; import {RocketMegapoolStorageLayout} from "./RocketMegapoolStorageLayout.sol"; /// @notice Contains the initialisation and delegate upgrade logic for megapools. /// All other calls are delegated to the node operator's current delegate or optionally the latest. contract RocketMegapoolProxy is RocketMegapoolProxyInterface, RocketMegapoolStorageLayout { // Events event EtherReceived(address indexed from, uint256 amount, uint256 time); event DelegateUpgraded(address oldDelegate, address newDelegate, uint256 time); event UseLatestUpdated(bool state, uint256 time); // Immutables address immutable private self; RocketStorageInterface immutable private rocketStorage; // Construct constructor (RocketStorageInterface _rocketStorage) { self = address(this); rocketStorage = _rocketStorage; } /// @dev Prevent direct calls to this contract modifier notSelf() { require(address(this) != self); _; } /// @dev Only allow access from the owning node address modifier onlyMegapoolOwner() { // Only the node operator can upgrade address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, "Only the node operator can access this method"); _; } /// @notice Sets up the initial delegate /// @param _nodeAddress The owner of this megapool function initialise(address _nodeAddress) external override notSelf { // Check input require(_nodeAddress != address(0), "Invalid node address"); require(!storageState, "Already initialised"); // Flag storage state as initialised and record node address storageState = true; nodeAddress = _nodeAddress; // Set the current delegate (checking it exists) address delegateAddress = getContractAddress("rocketMegapoolDelegate"); require(contractExists(delegateAddress), "Delegate contract does not exist"); rocketMegapoolDelegate = delegateAddress; } /// @notice Receive an ETH deposit receive() external payable notSelf { // Emit ether received event emit EtherReceived(msg.sender, msg.value, block.timestamp); } /// @notice Delegates all other calls to megapool delegate contract (or latest if flag is set) /// @param _input Transaction calldata that is passed directly to the delegate fallback(bytes calldata _input) external payable notSelf returns (bytes memory) { address delegateContract; // If useLatestDelegate is set, use the latest delegate contract otherwise use stored and check expiry if (useLatestDelegate) { delegateContract = getContractAddress("rocketMegapoolDelegate"); } else { // Force delegate upgrade on expiry if (getDelegateExpired()) { _delegateUpgrade(); } delegateContract = rocketMegapoolDelegate; } // Check for contract existence require(contractExists(delegateContract), "Delegate contract does not exist"); // Execute delegatecall on the delegate contract (bool success, bytes memory data) = delegateContract.delegatecall(_input); if (!success) { revert(getRevertMessage(data)); } return data; } /// @notice Upgrade this megapool to the latest network delegate contract function delegateUpgrade() public override notSelf { // Only owner can upgrade if delegate hasn't expired if (!getDelegateExpired()) { address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, "Only the node operator can access this method"); } // Perform upgrade _delegateUpgrade(); } /// @dev Internal implementation of delegate upgrade function _delegateUpgrade() internal { // Only succeed if there is a new delegate to upgrade to address oldDelegate = rocketMegapoolDelegate; address newDelegate = getContractAddress("rocketMegapoolDelegate"); require(oldDelegate != newDelegate, "Already using latest"); // Set new delegate rocketMegapoolDelegate = newDelegate; // Log event emit DelegateUpgraded(oldDelegate, newDelegate, block.timestamp); } /// @notice Sets the flag to automatically use the latest delegate contract or not /// @param _state If true, will always use the latest delegate contract function setUseLatestDelegate(bool _state) external override onlyMegapoolOwner notSelf { // Prevent modification if already set to desired state require(useLatestDelegate != _state, "Already set"); // Update state useLatestDelegate = _state; // Log event emit UseLatestUpdated(_state, block.timestamp); if (!_state) { // Upon disabling use latest, set their current delegate to the latest address newDelegate = getContractAddress("rocketMegapoolDelegate"); if (newDelegate != rocketMegapoolDelegate) { delegateUpgrade(); } } } /// @notice Returns true if this megapool always uses the latest delegate contract function getUseLatestDelegate() external override view returns (bool) { return useLatestDelegate; } /// @notice Returns the address of the megapool's stored delegate function getDelegate() external override view returns (address) { return rocketMegapoolDelegate; } /// @notice Returns the delegate which will be used when calling this megapool taking into account /// useLatestDelegate setting and expired delegate function getEffectiveDelegate() external override view returns (address) { if (useLatestDelegate || getDelegateExpired()) { return getContractAddress("rocketMegapoolDelegate"); } return rocketMegapoolDelegate; } /// @notice Returns true if the megapools current delegate has expired function getDelegateExpired() public view returns (bool) { RocketMegapoolDelegateBaseInterface megapoolDelegate = RocketMegapoolDelegateBaseInterface(rocketMegapoolDelegate); uint256 expiry = megapoolDelegate.getExpirationTime(); return expiry != 0 && block.timestamp >= expiry; } /// @dev Get the address of a Rocket Pool network contract function getContractAddress(string memory _contractName) private view returns (address) { address contractAddress = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); require(contractAddress != address(0x0), "Contract not found"); return contractAddress; } /// @dev Get a revert message from delegatecall return data function getRevertMessage(bytes memory _returnData) private pure returns (string memory) { if (_returnData.length < 68) { return "Transaction reverted silently"; } assembly { _returnData := add(_returnData, 0x04) } return abi.decode(_returnData, (string)); } /// @dev Returns true if contract exists at _contractAddress (if called during that contract's construction it will return a false negative) function contractExists(address _contractAddress) private view returns (bool) { uint32 codeSize; assembly { codeSize := extcodesize(_contractAddress) } return codeSize > 0; } } ================================================ FILE: contracts/contract/megapool/RocketMegapoolStorageLayout.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; // ****************************************************** // Note: This contract MUST only be appended to. All // deployed megapool contracts must maintain a // consistent storage layout with RocketMegapoolDelegate. // ****************************************************** /// @dev The RocketMegapool contract storage layout, shared by RocketMegapoolDelegate and RocketMegapoolBase abstract contract RocketMegapoolStorageLayout { // Status of individual validators enum Status { InQueue, PreStaked, Staking, Exited, Dissolved } // Information about individual validators struct ValidatorInfo { uint32 lastAssignmentTime; // Timestamp of when the last fund assignment took place uint32 lastRequestedValue; // Value in milliether last requested uint32 lastRequestedBond; // Value in milliether of the bond supplied for last request for funds uint32 depositValue; // Total amount deposited to beaconchain in gwei bool staked; // Whether the validator has staked the minimum required to begin validating (32 ETH) bool exited; // Whether the validator has exited the beacon chain bool inQueue; // Whether the validator is currently awaiting funds from the deposit pool bool inPrestake; // Whether the validator is currently awaiting the stake operation bool expressUsed; // Whether the last request for funds consumed an express ticket bool dissolved; // Whether the validator failed to prestake their initial deposit in time bool exiting; // Whether the validator is queued to exit on the beaconchain bool locked; // Whether the validator has been locked by the oDAO for not exiting uint64 exitBalance; // Final balance of the validator at withdrawable_epoch in gwei (amount returned to EL) uint64 lockedTime; // The slot this validator was challenged about its exit status } // // Delegate state // bool internal storageState; // Used to prevent direct calls to the delegate contract uint256 internal expirationTime; // Used to store the expiry timestamp of this delegate (0 meaning not expiring) // // Proxy state // address internal rocketMegapoolDelegate; // The current delegate contract address bool internal useLatestDelegate; // Whether this proxy always uses the latest delegate // // Megapool state // address internal nodeAddress; // Megapool owner uint32 internal numValidators; // Number of individual validators handled by this megapool uint32 internal numInactiveValidators; // Number of validators that are no longer contributing to bond requirement uint256 internal assignedValue; // ETH assigned from DP pending prestake/stake uint256 internal refundValue; // ETH refunded to the owner after a dissolution uint256 internal nodeBond; // Value of node ETH bond (including value yet to be assigned/deposited) uint256 internal userCapital; // Value of user supplied capital (including value yet to be assigned/deposited) uint256 internal nodeQueuedBond; // Value of node ETH bond (yet to be assigned/deposited to beacon chain) uint256 internal userQueuedCapital; // Value of user supplied capital (yet to be assigned/deposited to beacon chain) uint256 internal debt; // Amount the owner owes the DP uint256 internal lastDistributionTime; // The block of the last time a distribution of rewards was executed mapping(uint32 => ValidatorInfo) internal validators; mapping(uint32 => bytes) internal prestakeSignatures; mapping(uint32 => bytes) internal pubkeys; uint32 internal numLockedValidators; // Number of validators currently locked uint32 internal numExitingValidators; // Number of validators currently exiting uint256 internal __version1Boundary; // Unused full slot width boundary } ================================================ FILE: contracts/contract/minipool/RocketMinipoolBase.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "./RocketMinipoolStorageLayout.sol"; import "../../interface/RocketStorageInterface.sol"; import "../../interface/minipool/RocketMinipoolBaseInterface.sol"; /// @notice Contains the initialisation and delegate upgrade logic for minipools contract RocketMinipoolBase is RocketMinipoolBaseInterface, RocketMinipoolStorageLayout { // Events event EtherReceived(address indexed from, uint256 amount, uint256 time); event DelegateUpgraded(address oldDelegate, address newDelegate, uint256 time); event DelegateRolledBack(address oldDelegate, address newDelegate, uint256 time); // Store a reference to the address of RocketMinipoolBase itself to prevent direct calls to this contract address immutable self; constructor () { self = address(this); } /// @dev Prevent direct calls to this contract modifier notSelf() { require(address(this) != self); _; } /// @dev Only allow access from the owning node address modifier onlyMinipoolOwner() { // Only the node operator can upgrade address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, "Only the node operator can access this method"); _; } /// @notice Sets up starting delegate contract and then delegates initialisation to it function initialise(address _rocketStorage, address _nodeAddress) external override notSelf { // Check input require(_nodeAddress != address(0), "Invalid node address"); require(storageState == StorageState.Undefined, "Already initialised"); // Set storage state to uninitialised storageState = StorageState.Uninitialised; // Set rocketStorage rocketStorage = RocketStorageInterface(_rocketStorage); // Set the current delegate address delegateAddress = getContractAddress("rocketMinipoolDelegate"); rocketMinipoolDelegate = delegateAddress; // Check for contract existence require(contractExists(delegateAddress), "Delegate contract does not exist"); // Call initialise on delegate (bool success, bytes memory data) = delegateAddress.delegatecall(abi.encodeWithSignature('initialise(address)', _nodeAddress)); if (!success) { revert(getRevertMessage(data)); } } /// @notice Receive an ETH deposit receive() external payable notSelf { // Emit ether received event emit EtherReceived(msg.sender, msg.value, block.timestamp); } /// @notice Upgrade this minipool to the latest network delegate contract function delegateUpgrade() external override onlyMinipoolOwner notSelf { // Set previous address rocketMinipoolDelegatePrev = rocketMinipoolDelegate; // Set new delegate rocketMinipoolDelegate = getContractAddress("rocketMinipoolDelegate"); // Verify require(rocketMinipoolDelegate != rocketMinipoolDelegatePrev, "New delegate is the same as the existing one"); // Log event emit DelegateUpgraded(rocketMinipoolDelegatePrev, rocketMinipoolDelegate, block.timestamp); } /// @notice Rollback to previous delegate contract function delegateRollback() external override onlyMinipoolOwner notSelf { // Make sure they have upgraded before require(rocketMinipoolDelegatePrev != address(0x0), "Previous delegate contract is not set"); // Store original address originalDelegate = rocketMinipoolDelegate; // Update delegate to previous and zero out previous rocketMinipoolDelegate = rocketMinipoolDelegatePrev; rocketMinipoolDelegatePrev = address(0x0); // Log event emit DelegateRolledBack(originalDelegate, rocketMinipoolDelegate, block.timestamp); } /// @notice Sets the flag to automatically use the latest delegate contract or not /// @param _setting If true, will always use the latest delegate contract function setUseLatestDelegate(bool _setting) external override onlyMinipoolOwner notSelf { useLatestDelegate = _setting; } /// @notice Returns true if this minipool always uses the latest delegate contract function getUseLatestDelegate() external override view returns (bool) { return useLatestDelegate; } /// @notice Returns the address of the minipool's stored delegate function getDelegate() external override view returns (address) { return rocketMinipoolDelegate; } /// @notice Returns the address of the minipool's previous delegate (or address(0) if not set) function getPreviousDelegate() external override view returns (address) { return rocketMinipoolDelegatePrev; } /// @notice Returns the delegate which will be used when calling this minipool taking into account useLatestDelegate setting function getEffectiveDelegate() external override view returns (address) { return useLatestDelegate ? getContractAddress("rocketMinipoolDelegate") : rocketMinipoolDelegate; } /// @notice Delegates all calls to minipool delegate contract (or latest if flag is set) fallback(bytes calldata _input) external payable notSelf returns (bytes memory) { // If useLatestDelegate is set, use the latest delegate contract address delegateContract = useLatestDelegate ? getContractAddress("rocketMinipoolDelegate") : rocketMinipoolDelegate; // Check for contract existence require(contractExists(delegateContract), "Delegate contract does not exist"); // Execute delegatecall (bool success, bytes memory data) = delegateContract.delegatecall(_input); if (!success) { revert(getRevertMessage(data)); } return data; } /// @dev Get the address of a Rocket Pool network contract function getContractAddress(string memory _contractName) private view returns (address) { address contractAddress = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); require(contractAddress != address(0x0), "Contract not found"); return contractAddress; } /// @dev Get a revert message from delegatecall return data function getRevertMessage(bytes memory _returnData) private pure returns (string memory) { if (_returnData.length < 68) { return "Transaction reverted silently"; } assembly { _returnData := add(_returnData, 0x04) } return abi.decode(_returnData, (string)); } /// @dev Returns true if contract exists at _contractAddress (if called during that contract's construction it will return a false negative) function contractExists(address _contractAddress) private view returns (bool) { uint32 codeSize; assembly { codeSize := extcodesize(_contractAddress) } return codeSize > 0; } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolBondReducer.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; pragma abicoder v2; import {RocketBase} from "../RocketBase.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketMinipoolInterface} from "../../interface/minipool/RocketMinipoolInterface.sol"; import {RocketMinipoolBondReducerInterface} from "../../interface/minipool/RocketMinipoolBondReducerInterface.sol"; import {RocketNodeDepositInterface} from "../../interface/node/RocketNodeDepositInterface.sol"; import {RocketDAONodeTrustedSettingsMinipoolInterface} from "../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import {RocketDAONodeTrustedInterface} from "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import {RocketDAOProtocolSettingsRewardsInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import {RocketDAOProtocolSettingsMinipoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import {RocketMinipoolManagerInterface} from "../../interface/minipool/RocketMinipoolManagerInterface.sol"; /// @notice Handles bond reduction window and trusted node cancellation contract RocketMinipoolBondReducer is RocketBase, RocketMinipoolBondReducerInterface { // Events event CancelReductionVoted(address indexed minipool, address indexed member, uint256 time); event ReductionCancelled(address indexed minipool, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Always reverts function beginReduceBondAmount(address _minipoolAddress, uint256 _newBondAmount) override external onlyLatestContract("rocketMinipoolBondReducer", address(this)) { revert("Minipool bond reductions are no longer available"); } /// @notice Always reverts function reduceBondAmount() override external onlyRegisteredMinipool(msg.sender) onlyLatestContract("rocketMinipoolBondReducer", address(this)) returns (uint256) { revert("Minipool bond reductions are no longer available"); } /// @notice Always reverts function voteCancelReduction(address _minipoolAddress) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketMinipoolBondReducer", address(this)) { revert("Minipool bond reductions are no longer available"); } /// @notice Returns the timestamp of when a given minipool began their bond reduction waiting period /// @param _minipoolAddress Address of the minipool to query function getReduceBondTime(address _minipoolAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("minipool.bond.reduction.time", _minipoolAddress))); } /// @notice Returns the new bond that a given minipool has indicated they are reducing to /// @param _minipoolAddress Address of the minipool to query function getReduceBondValue(address _minipoolAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("minipool.bond.reduction.value", _minipoolAddress))); } /// @notice Returns true if the given minipool has had it's bond reduction cancelled by the oDAO /// @param _minipoolAddress Address of the minipool to query function getReduceBondCancelled(address _minipoolAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("minipool.bond.reduction.cancelled", address(_minipoolAddress)))); } /// @notice Always returns false /// @param _minipoolAddress Address of the minipool function canReduceBondAmount(address _minipoolAddress) override public view returns (bool) { return false; } /// @notice Returns a timestamp of when the given minipool last performed a bond reduction /// @param _minipoolAddress The address of the minipool to query /// @return Unix timestamp of last bond reduction (or 0 if never reduced) function getLastBondReductionTime(address _minipoolAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("minipool.last.bond.reduction.time", _minipoolAddress))); } /// @notice Returns the previous bond value of the given minipool on their last bond reduction /// @param _minipoolAddress The address of the minipool to query /// @return Previous bond value in wei (or 0 if never reduced) function getLastBondReductionPrevValue(address _minipoolAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("minipool.last.bond.reduction.prev.value", _minipoolAddress))); } /// @notice Returns the previous node fee of the given minipool on their last bond reduction /// @param _minipoolAddress The address of the minipool to query /// @return Previous node fee function getLastBondReductionPrevNodeFee(address _minipoolAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("minipool.last.bond.reduction.prev.fee", _minipoolAddress))); } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolDelegate.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "./RocketMinipoolStorageLayout.sol"; import "../../interface/casper/DepositInterface.sol"; import "../../interface/deposit/RocketDepositPoolInterface.sol"; import "../../interface/minipool/RocketMinipoolInterface.sol"; import "../../interface/minipool/RocketMinipoolManagerInterface.sol"; import "../../interface/minipool/RocketMinipoolQueueInterface.sol"; import "../../interface/minipool/RocketMinipoolPenaltyInterface.sol"; import "../../interface/node/RocketNodeStakingInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../interface/network/RocketNetworkFeesInterface.sol"; import "../../interface/token/RocketTokenRETHInterface.sol"; import "../../types/MinipoolDeposit.sol"; import "../../types/MinipoolStatus.sol"; import "../../interface/node/RocketNodeDepositInterface.sol"; import "../../interface/minipool/RocketMinipoolBondReducerInterface.sol"; /// @notice Provides the logic for each individual minipool in the Rocket Pool network /// @dev Minipools exclusively DELEGATECALL into this contract it is never called directly contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolInterface { // Constants uint8 public constant override version = 3; // Used to identify which delegate contract each minipool is using uint256 internal constant calcBase = 1 ether; // Fixed point arithmetic uses this for value for precision uint256 internal constant legacyPrelaunchAmount = 16 ether; // The amount of ETH initially deposited when minipool is created (for legacy minipools) uint256 internal constant scrubPenalty = 2.4 ether; // Amount of ETH penalised during a successful scrub // Libs using SafeMath for uint; // Events event StatusUpdated(uint8 indexed status, uint256 time); event ScrubVoted(address indexed member, uint256 time); event BondReduced(uint256 previousBondAmount, uint256 newBondAmount, uint256 time); event MinipoolScrubbed(uint256 time); event MinipoolPrestaked(bytes validatorPubkey, bytes validatorSignature, bytes32 depositDataRoot, uint256 amount, bytes withdrawalCredentials, uint256 time); event MinipoolPromoted(uint256 time); event MinipoolVacancyPrepared(uint256 bondAmount, uint256 currentBalance, uint256 time); event EtherDeposited(address indexed from, uint256 amount, uint256 time); event EtherWithdrawn(address indexed to, uint256 amount, uint256 time); event EtherWithdrawalProcessed(address indexed executed, uint256 nodeAmount, uint256 userAmount, uint256 totalBalance, uint256 time); // Status getters function getStatus() override external view returns (MinipoolStatus) { return status; } function getFinalised() override external view returns (bool) { return finalised; } function getStatusBlock() override external view returns (uint256) { return statusBlock; } function getStatusTime() override external view returns (uint256) { return statusTime; } function getScrubVoted(address _member) override external view returns (bool) { return memberScrubVotes[_member]; } // Deposit type getter function getDepositType() override external view returns (MinipoolDeposit) { return depositType; } // Node detail getters function getNodeAddress() override external view returns (address) { return nodeAddress; } function getNodeFee() override external view returns (uint256) { return nodeFee; } function getNodeDepositBalance() override external view returns (uint256) { return nodeDepositBalance; } function getNodeRefundBalance() override external view returns (uint256) { return nodeRefundBalance; } function getNodeDepositAssigned() override external view returns (bool) { return userDepositAssignedTime != 0; } function getPreLaunchValue() override external view returns (uint256) { return preLaunchValue; } function getNodeTopUpValue() override external view returns (uint256) { return nodeDepositBalance.sub(preLaunchValue); } function getVacant() override external view returns (bool) { return vacant; } function getPreMigrationBalance() override external view returns (uint256) { return preMigrationBalance; } function getUserDistributed() override external view returns (bool) { return userDistributed; } // User deposit detail getters function getUserDepositBalance() override public view returns (uint256) { if (depositType == MinipoolDeposit.Variable) { return userDepositBalance; } else { return userDepositBalanceLegacy; } } function getUserDepositAssigned() override external view returns (bool) { return userDepositAssignedTime != 0; } function getUserDepositAssignedTime() override external view returns (uint256) { return userDepositAssignedTime; } function getTotalScrubVotes() override external view returns (uint256) { return totalScrubVotes; } /// @dev Prevent direct calls to this contract modifier onlyInitialised() { require(storageState == StorageState.Initialised, "Storage state not initialised"); _; } /// @dev Prevent multiple calls to initialise modifier onlyUninitialised() { require(storageState == StorageState.Uninitialised, "Storage state already initialised"); _; } /// @dev Only allow access from the owning node address modifier onlyMinipoolOwner(address _nodeAddress) { require(_nodeAddress == nodeAddress, "Invalid minipool owner"); _; } /// @dev Only allow access from the owning node address or their withdrawal address modifier onlyMinipoolOwnerOrWithdrawalAddress(address _nodeAddress) { require(_nodeAddress == nodeAddress || _nodeAddress == rocketStorage.getNodeWithdrawalAddress(nodeAddress), "Invalid minipool owner"); _; } /// @dev Only allow access from the latest version of the specified Rocket Pool contract modifier onlyLatestContract(string memory _contractName, address _contractAddress) { require(_contractAddress == getContractAddress(_contractName), "Invalid or outdated contract"); _; } /// @dev Get the address of a Rocket Pool network contract /// @param _contractName The internal name of the contract to retrieve the address for function getContractAddress(string memory _contractName) private view returns (address) { address contractAddress = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); require(contractAddress != address(0x0), "Contract not found"); return contractAddress; } /// @dev Called once on creation to initialise starting state /// @param _nodeAddress The address of the node operator who will own this minipool function initialise(address _nodeAddress) override external onlyUninitialised { // Check parameters require(_nodeAddress != address(0x0), "Invalid node address"); // Load contracts RocketNetworkFeesInterface rocketNetworkFees = RocketNetworkFeesInterface(getContractAddress("rocketNetworkFees")); // Set initial status status = MinipoolStatus.Initialised; statusBlock = block.number; statusTime = block.timestamp; // Set details depositType = MinipoolDeposit.Variable; nodeAddress = _nodeAddress; nodeFee = rocketNetworkFees.getNodeFee(); // Set the rETH address rocketTokenRETH = getContractAddress("rocketTokenRETH"); // Set local copy of penalty contract rocketMinipoolPenalty = getContractAddress("rocketMinipoolPenalty"); // Intialise storage state storageState = StorageState.Initialised; } /// @notice Performs the initial pre-stake on the beacon chain to set the withdrawal credentials /// @param _bondValue The amount of the stake which will be provided by the node operator /// @param _validatorPubkey The public key of the validator /// @param _validatorSignature A signature over the deposit message object /// @param _depositDataRoot The hash tree root of the deposit data object function preDeposit(uint256 _bondValue, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external payable onlyLatestContract("rocketNodeDeposit", msg.sender) onlyInitialised { // Check current status & node deposit status require(status == MinipoolStatus.Initialised, "The pre-deposit can only be made while initialised"); require(preLaunchValue == 0, "Pre-deposit already performed"); // Update node deposit details nodeDepositBalance = _bondValue; preLaunchValue = msg.value; // Emit ether deposited event emit EtherDeposited(msg.sender, preLaunchValue, block.timestamp); // Perform the pre-stake to lock in withdrawal credentials on beacon chain preStake(_validatorPubkey, _validatorSignature, _depositDataRoot); } /// @notice Performs the second deposit which provides the validator with the remaining balance to become active function deposit() override external payable onlyLatestContract("rocketDepositPool", msg.sender) onlyInitialised { // Check current status & node deposit status require(status == MinipoolStatus.Initialised, "The node deposit can only be assigned while initialised"); require(userDepositAssignedTime == 0, "The user deposit has already been assigned"); // Set the minipool status to prelaunch (ready for node to call `stake()`) setStatus(MinipoolStatus.Prelaunch); // Update deposit details userDepositBalance = msg.value.add(preLaunchValue).sub(nodeDepositBalance); userDepositAssignedTime = block.timestamp; // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); } /// @notice Assign user deposited ETH to the minipool and mark it as prelaunch /// @dev No longer used in "Variable" type minipools (only retained for legacy minipools still in queue) function userDeposit() override external payable onlyLatestContract("rocketDepositPool", msg.sender) onlyInitialised { // Check current status & user deposit status require(status >= MinipoolStatus.Initialised && status <= MinipoolStatus.Staking, "The user deposit can only be assigned while initialised, in prelaunch, or staking"); require(userDepositAssignedTime == 0, "The user deposit has already been assigned"); // Progress initialised minipool to prelaunch if (status == MinipoolStatus.Initialised) { setStatus(MinipoolStatus.Prelaunch); } // Update user deposit details userDepositBalance = msg.value; userDepositAssignedTime = block.timestamp; // Refinance full minipool if (depositType == MinipoolDeposit.Full) { // Update node balances nodeDepositBalance = nodeDepositBalance.sub(msg.value); nodeRefundBalance = nodeRefundBalance.add(msg.value); } // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); } /// @notice Refund node ETH refinanced from user deposited ETH function refund() override external onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) onlyInitialised { // Check refund balance require(nodeRefundBalance > 0, "No amount of the node deposit is available for refund"); // If this minipool was distributed by a user, force finalisation on the node operator if (!finalised && userDistributed) { // Note: _refund is called inside _finalise _finalise(); } else { // Refund node _refund(); } } /// @notice Called to slash node operator's RPL balance if withdrawal balance was less than user deposit function slash() external override onlyInitialised { // Check there is a slash balance require(nodeSlashBalance > 0, "No balance to slash"); // Perform slash _slash(); } /// @notice Returns true when `stake()` can be called by node operator taking into consideration the scrub period function canStake() override external view onlyInitialised returns (bool) { // Check status if (status != MinipoolStatus.Prelaunch) { return false; } // Get contracts RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); // Get scrub period uint256 scrubPeriod = rocketDAONodeTrustedSettingsMinipool.getScrubPeriod(); // Check if we have been in prelaunch status for long enough return block.timestamp > statusTime + scrubPeriod; } /// @notice Returns true when `promote()` can be called by node operator taking into consideration the scrub period function canPromote() override external view onlyInitialised returns (bool) { // Check status if (status != MinipoolStatus.Prelaunch) { return false; } // Get contracts RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); // Get scrub period uint256 scrubPeriod = rocketDAONodeTrustedSettingsMinipool.getPromotionScrubPeriod(); // Check if we have been in prelaunch status for long enough return block.timestamp > statusTime + scrubPeriod; } /// @notice Progress the minipool to staking, sending its ETH deposit to the deposit contract. Only accepts calls from the minipool owner (node) while in prelaunch and once scrub period has ended /// @param _validatorSignature A signature over the deposit message object /// @param _depositDataRoot The hash tree root of the deposit data object function stake(bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external onlyMinipoolOwner(msg.sender) onlyInitialised { // Get contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); { // Get scrub period RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); uint256 scrubPeriod = rocketDAONodeTrustedSettingsMinipool.getScrubPeriod(); // Check current status require(status == MinipoolStatus.Prelaunch, "The minipool can only begin staking while in prelaunch"); require(block.timestamp > statusTime + scrubPeriod, "Not enough time has passed to stake"); require(!vacant, "Cannot stake a vacant minipool"); } // Progress to staking setStatus(MinipoolStatus.Staking); // Load contracts DepositInterface casperDeposit = DepositInterface(getContractAddress("casperDeposit")); RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Get launch amount uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); uint256 depositAmount; // Legacy minipools had a prestake equal to the bond amount if (depositType == MinipoolDeposit.Variable) { depositAmount = launchAmount.sub(preLaunchValue); } else { depositAmount = launchAmount.sub(legacyPrelaunchAmount); } // Check minipool balance require(address(this).balance >= depositAmount, "Insufficient balance to begin staking"); // Retrieve validator pubkey from storage bytes memory validatorPubkey = rocketMinipoolManager.getMinipoolPubkey(address(this)); // Send staking deposit to casper casperDeposit.deposit{value : depositAmount}(validatorPubkey, rocketMinipoolManager.getMinipoolWithdrawalCredentials(address(this)), _validatorSignature, _depositDataRoot); // Increment node's number of staking minipools rocketMinipoolManager.incrementNodeStakingMinipoolCount(nodeAddress); } /// @dev Sets the bond value and vacancy flag on this minipool /// @param _bondAmount The bond amount selected by the node operator /// @param _currentBalance The current balance of the validator on the beaconchain (will be checked by oDAO and scrubbed if not correct) function prepareVacancy(uint256 _bondAmount, uint256 _currentBalance) override external onlyLatestContract("rocketMinipoolManager", msg.sender) onlyInitialised { // Check status require(status == MinipoolStatus.Initialised, "Must be in initialised status"); // Sanity check that refund balance is zero require(nodeRefundBalance == 0, "Refund balance not zero"); // Check balance RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); require(_currentBalance >= launchAmount, "Balance is too low"); // Store bond amount nodeDepositBalance = _bondAmount; // Calculate user amount from launch amount userDepositBalance = launchAmount.sub(nodeDepositBalance); // Flag as vacant vacant = true; preMigrationBalance = _currentBalance; // Refund the node whatever rewards they have accrued prior to becoming a RP validator nodeRefundBalance = _currentBalance.sub(launchAmount); // Set status to preLaunch setStatus(MinipoolStatus.Prelaunch); // Emit event emit MinipoolVacancyPrepared(_bondAmount, _currentBalance, block.timestamp); } /// @dev Promotes this minipool to a complete minipool function promote() override external onlyMinipoolOwner(msg.sender) onlyInitialised { // Check status require(status == MinipoolStatus.Prelaunch, "The minipool can only promote while in prelaunch"); require(vacant, "Cannot promote a non-vacant minipool"); // Get contracts RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); // Clear vacant flag vacant = false; // Check scrub period uint256 scrubPeriod = rocketDAONodeTrustedSettingsMinipool.getPromotionScrubPeriod(); require(block.timestamp > statusTime + scrubPeriod, "Not enough time has passed to promote"); // Progress to staking setStatus(MinipoolStatus.Staking); // Increment node's number of staking minipools RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); rocketMinipoolManager.incrementNodeStakingMinipoolCount(nodeAddress); // Set deposit assigned time userDepositAssignedTime = block.timestamp; // Increase node operator's deposit credit RocketNodeDepositInterface rocketNodeDepositInterface = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit")); rocketNodeDepositInterface.increaseDepositCreditBalance(nodeAddress, userDepositBalance); // Remove from vacant set rocketMinipoolManager.removeVacantMinipool(); // Emit event emit MinipoolPromoted(block.timestamp); } /// @dev Stakes the balance of this minipool into the deposit contract to set withdrawal credentials to this contract /// @param _validatorSignature A signature over the deposit message object /// @param _depositDataRoot The hash tree root of the deposit data object function preStake(bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) internal { // Load contracts DepositInterface casperDeposit = DepositInterface(getContractAddress("casperDeposit")); RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Set minipool pubkey rocketMinipoolManager.setMinipoolPubkey(_validatorPubkey); // Get withdrawal credentials bytes memory withdrawalCredentials = rocketMinipoolManager.getMinipoolWithdrawalCredentials(address(this)); // Send staking deposit to casper casperDeposit.deposit{value : preLaunchValue}(_validatorPubkey, withdrawalCredentials, _validatorSignature, _depositDataRoot); // Emit event emit MinipoolPrestaked(_validatorPubkey, _validatorSignature, _depositDataRoot, preLaunchValue, withdrawalCredentials, block.timestamp); } /// @notice Distributes the contract's balance. /// If balance is greater or equal to 8 ETH, the NO can call to distribute capital and finalise the minipool. /// If balance is greater or equal to 8 ETH, users who have called `beginUserDistribute` and waited the required /// amount of time can call to distribute capital. /// If balance is lower than 8 ETH, can be called by anyone and is considered a partial withdrawal and funds are /// split as rewards. /// @param _rewardsOnly If set to true, will revert if balance is not being treated as rewards function distributeBalance(bool _rewardsOnly) override external onlyInitialised { // Get node withdrawal address address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); bool ownerCalling = msg.sender == nodeAddress || msg.sender == nodeWithdrawalAddress; // If dissolved, distribute everything to the owner if (status == MinipoolStatus.Dissolved) { require(ownerCalling, "Only owner can distribute dissolved minipool"); distributeToOwner(); return; } // Can only be called while in staking status require(status == MinipoolStatus.Staking, "Minipool must be staking"); // Get withdrawal amount, we must also account for a possible node refund balance on the contract uint256 totalBalance = address(this).balance.sub(nodeRefundBalance); if (totalBalance >= 8 ether) { // Prevent funding front runs of distribute balance require(!_rewardsOnly, "Balance exceeds 8 ether"); // Consider this a full withdrawal _distributeBalance(totalBalance); if (ownerCalling) { // Finalise the minipool if the owner is calling _finalise(); } else { // Require user wait period to pass before allowing user to distribute require(userDistributeAllowed(), "Only owner can distribute right now"); // Mark this minipool as having been distributed by a user userDistributed = true; } } else { // Just a partial withdraw distributeSkimmedRewards(); // If node operator is calling, save a tx by calling refund immediately if (ownerCalling && nodeRefundBalance > 0) { _refund(); } } // Reset distribute waiting period userDistributeTime = 0; } /// @dev Distribute the entire balance to the minipool owner function distributeToOwner() internal { // Get balance uint256 balance = address(this).balance; // Get node withdrawal address address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); // Transfer balance (bool success,) = nodeWithdrawalAddress.call{value : balance}(""); require(success, "Node ETH balance was not successfully transferred to node operator"); // Emit ether withdrawn event emit EtherWithdrawn(nodeWithdrawalAddress, balance, block.timestamp); } /// @notice Allows a user (other than the owner of this minipool) to signal they want to call distribute. /// After waiting the required period, anyone may then call `distributeBalance()`. function beginUserDistribute() override external onlyInitialised { require(status == MinipoolStatus.Staking, "Minipool must be staking"); uint256 totalBalance = address(this).balance.sub(nodeRefundBalance); require (totalBalance >= 8 ether, "Balance too low"); // Prevent calls resetting distribute time before window has passed RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); uint256 timeElapsed = block.timestamp.sub(userDistributeTime); require(rocketDAOProtocolSettingsMinipool.hasUserDistributeWindowPassed(timeElapsed), "User distribution already pending"); // Store current time userDistributeTime = block.timestamp; } /// @notice Returns true if enough time has passed for a user to distribute function userDistributeAllowed() override public view returns (bool) { // Get contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Calculate if time elapsed since call to `beginUserDistribute` is within the allowed window uint256 timeElapsed = block.timestamp.sub(userDistributeTime); return(rocketDAOProtocolSettingsMinipool.isWithinUserDistributeWindow(timeElapsed)); } /// @notice Allows the owner of this minipool to finalise it after a user has manually distributed the balance function finalise() override external onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) onlyInitialised { require(userDistributed, "Can only manually finalise after user distribution"); _finalise(); } /// @dev Perform any slashings, refunds, and unlock NO's stake function _finalise() private { // Get contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Can only finalise the pool once require(!finalised, "Minipool has already been finalised"); // Set finalised flag finalised = true; // If slash is required then perform it if (nodeSlashBalance > 0) { _slash(); } // Refund node operator if required if (nodeRefundBalance > 0) { _refund(); } // Send any left over ETH to rETH contract if (address(this).balance > 0) { // Send user amount to rETH contract payable(rocketTokenRETH).transfer(address(this).balance); } // Trigger a deposit of excess collateral from rETH contract to deposit pool RocketTokenRETHInterface(rocketTokenRETH).depositExcessCollateral(); // Unlock node operator's RPL rocketMinipoolManager.incrementNodeFinalisedMinipoolCount(nodeAddress); rocketMinipoolManager.decrementNodeStakingMinipoolCount(nodeAddress); } /// @dev Distributes balance to user and node operator /// @param _balance The amount to distribute function _distributeBalance(uint256 _balance) private { // Deposit amounts uint256 nodeAmount = 0; uint256 userCapital = getUserDepositBalance(); // Check if node operator was slashed if (_balance < userCapital) { // Only slash on first call to distribute if (withdrawalBlock == 0) { // Record shortfall for slashing nodeSlashBalance = userCapital.sub(_balance); } } else { // Calculate node's share of the balance nodeAmount = _calculateNodeShare(_balance); } // User amount is what's left over from node's share uint256 userAmount = _balance.sub(nodeAmount); // Pay node operator via refund nodeRefundBalance = nodeRefundBalance.add(nodeAmount); // Pay user amount to rETH contract if (userAmount > 0) { // Send user amount to rETH contract payable(rocketTokenRETH).transfer(userAmount); } // Save block to prevent multiple withdrawals within a few blocks withdrawalBlock = block.number; // Log it emit EtherWithdrawalProcessed(msg.sender, nodeAmount, userAmount, _balance, block.timestamp); } /// @notice Given a balance, this function returns what portion of it belongs to the node taking into /// consideration the 8 ether reward threshold, the minipool's commission rate and any penalties it may have /// attracted. Another way of describing this function is that if this contract's balance was /// `_balance + nodeRefundBalance` this function would return how much of that balance would be paid to the node /// operator if a distribution occurred /// @param _balance The balance to calculate the node share of. Should exclude nodeRefundBalance function calculateNodeShare(uint256 _balance) override public view returns (uint256) { // Sub 8 ether balance is treated as rewards if (_balance < 8 ether) { return calculateNodeRewards(nodeDepositBalance, getUserDepositBalance(), _balance); } else { return _calculateNodeShare(_balance); } } /// @notice Performs the same calculation as `calculateNodeShare` but on the user side /// @param _balance The balance to calculate the node share of. Should exclude nodeRefundBalance function calculateUserShare(uint256 _balance) override external view returns (uint256) { // User's share is just the balance minus node refund minus node's share return _balance.sub(calculateNodeShare(_balance)); } /// @dev Given a balance, this function returns what portion of it belongs to the node taking into /// consideration the minipool's commission rate and any penalties it may have attracted /// @param _balance The balance to calculate the node share of (with nodeRefundBalance already subtracted) function _calculateNodeShare(uint256 _balance) internal view returns (uint256) { uint256 userCapital = getUserDepositBalance(); uint256 nodeCapital = nodeDepositBalance; uint256 nodeShare = 0; // Calculate the total capital (node + user) uint256 capital = userCapital.add(nodeCapital); if (_balance > capital) { // Total rewards to share uint256 rewards = _balance.sub(capital); nodeShare = nodeCapital.add(calculateNodeRewards(nodeCapital, userCapital, rewards)); } else if (_balance > userCapital) { nodeShare = _balance.sub(userCapital); } // Check if node has an ETH penalty uint256 penaltyRate = RocketMinipoolPenaltyInterface(rocketMinipoolPenalty).getPenaltyRate(address(this)); if (penaltyRate > 0) { uint256 penaltyAmount = nodeShare.mul(penaltyRate).div(calcBase); if (penaltyAmount > nodeShare) { penaltyAmount = nodeShare; } nodeShare = nodeShare.sub(penaltyAmount); } return nodeShare; } /// @dev Calculates what portion of rewards should be paid to the node operator given a capital ratio /// @param _nodeCapital The node supplied portion of the capital /// @param _userCapital The user supplied portion of the capital /// @param _rewards The amount of rewards to split function calculateNodeRewards(uint256 _nodeCapital, uint256 _userCapital, uint256 _rewards) internal view returns (uint256) { // Calculate node and user portion based on proportions of capital provided uint256 nodePortion = _rewards.mul(_nodeCapital).div(_userCapital.add(_nodeCapital)); uint256 userPortion = _rewards.sub(nodePortion); // Calculate final node amount as combination of node capital, node share and commission on user share return nodePortion.add(userPortion.mul(nodeFee).div(calcBase)); } /// @notice Dissolve the minipool, returning user deposited ETH to the deposit pool. function dissolve() override external onlyInitialised { // Check current status require(status == MinipoolStatus.Prelaunch, "The minipool can only be dissolved while in prelaunch"); // Load contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Check if minipool is timed out require(block.timestamp.sub(statusTime) >= rocketDAOProtocolSettingsMinipool.getLaunchTimeout(), "The minipool can only be dissolved once it has timed out"); // Perform the dissolution _dissolve(0); } /// @notice Withdraw node balances from the minipool and close it. Only accepts calls from the owner function close() override external onlyMinipoolOwner(msg.sender) onlyInitialised { // Check current status require(status == MinipoolStatus.Dissolved, "The minipool can only be closed while dissolved"); // Distribute funds to owner distributeToOwner(); // Destroy minipool RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); require(rocketMinipoolManager.getMinipoolExists(address(this)), "Minipool already closed"); rocketMinipoolManager.destroyMinipool(); // Clear state nodeDepositBalance = 0; nodeRefundBalance = 0; userDepositBalance = 0; userDepositBalanceLegacy = 0; userDepositAssignedTime = 0; } /// @notice Can be called by trusted nodes to scrub this minipool if its withdrawal credentials are not set correctly function voteScrub() override external onlyInitialised { // Check current status require(status == MinipoolStatus.Prelaunch, "The minipool can only be scrubbed while in prelaunch"); // Get contracts RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool")); // Must be a trusted member require(rocketDAONode.getMemberIsValid(msg.sender), "Not a trusted member"); // Can only vote once require(!memberScrubVotes[msg.sender], "Member has already voted to scrub"); memberScrubVotes[msg.sender] = true; // Emit event emit ScrubVoted(msg.sender, block.timestamp); // Check if required quorum has voted uint256 quorum = rocketDAONode.getMemberCount().mul(rocketDAONodeTrustedSettingsMinipool.getScrubQuorum()).div(calcBase); if (totalScrubVotes.add(1) > quorum) { if (!vacant && rocketDAONodeTrustedSettingsMinipool.getScrubPenaltyEnabled()){ _dissolve(scrubPenalty); } else { _dissolve(0); } // Emit event emit MinipoolScrubbed(block.timestamp); } else { // Increment total totalScrubVotes = totalScrubVotes.add(1); } } /// @notice Reduces the ETH bond amount and credits the owner the difference function reduceBondAmount() override external onlyMinipoolOwner(msg.sender) onlyInitialised { require(status == MinipoolStatus.Staking, "Minipool must be staking"); // If balance is greater than 8 ether, it is assumed to be capital not skimmed rewards. So prevent reduction uint256 totalBalance = address(this).balance.sub(nodeRefundBalance); require(totalBalance < 8 ether, "Cannot reduce bond with balance of 8 ether or more"); // Distribute any skimmed rewards distributeSkimmedRewards(); // Approve reduction and handle external state changes RocketMinipoolBondReducerInterface rocketBondReducer = RocketMinipoolBondReducerInterface(getContractAddress("rocketMinipoolBondReducer")); uint256 previousBond = nodeDepositBalance; uint256 newBond = rocketBondReducer.reduceBondAmount(); // Update user/node balances userDepositBalance = getUserDepositBalance().add(previousBond.sub(newBond)); nodeDepositBalance = newBond; // Reset node fee to current network rate RocketNetworkFeesInterface rocketNetworkFees = RocketNetworkFeesInterface(getContractAddress("rocketNetworkFees")); uint256 prevFee = nodeFee; uint256 newFee = rocketNetworkFees.getNodeFee(); nodeFee = newFee; // Update staking minipool counts and fee numerator RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); rocketMinipoolManager.updateNodeStakingMinipoolCount(previousBond, newBond, prevFee, newFee); // Break state to prevent rollback exploit if (depositType != MinipoolDeposit.Variable) { userDepositBalanceLegacy = 2 ** 256 - 1; depositType = MinipoolDeposit.Variable; } // Emit event emit BondReduced(previousBond, newBond, block.timestamp); } /// @dev Distributes the current contract balance based on capital ratio and node fee function distributeSkimmedRewards() internal { uint256 rewards = address(this).balance.sub(nodeRefundBalance); uint256 nodeShare = calculateNodeRewards(nodeDepositBalance, getUserDepositBalance(), rewards); // Pay node operator via refund mechanism nodeRefundBalance = nodeRefundBalance.add(nodeShare); // Deposit user share into rETH contract payable(rocketTokenRETH).transfer(rewards.sub(nodeShare)); } /// @dev Set the minipool's current status /// @param _status The new status function setStatus(MinipoolStatus _status) private { // Update status status = _status; statusBlock = block.number; statusTime = block.timestamp; // Emit status updated event emit StatusUpdated(uint8(_status), block.timestamp); } /// @dev Transfer refunded ETH balance to the node operator function _refund() private { // Prevent vacant minipools from calling require(vacant == false, "Vacant minipool cannot refund"); // Update refund balance uint256 refundAmount = nodeRefundBalance; nodeRefundBalance = 0; // Get node withdrawal address address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); // Transfer refund amount (bool success,) = nodeWithdrawalAddress.call{value : refundAmount}(""); require(success, "ETH refund amount was not successfully transferred to node operator"); // Emit ether withdrawn event emit EtherWithdrawn(nodeWithdrawalAddress, refundAmount, block.timestamp); } /// @dev Slash node operator's RPL balance based on nodeSlashBalance function _slash() private { // Get contracts RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); // Slash required amount and reset storage value uint256 slashAmount = nodeSlashBalance; nodeSlashBalance = 0; rocketNodeStaking.slashRPL(nodeAddress, slashAmount); } /// @dev Dissolve this minipool /// @param _penalty An additional amount of ETH to send back to the deposit pool (unused for vacant minipools) function _dissolve(uint256 _penalty) private { // Get contracts RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); // Progress to dissolved setStatus(MinipoolStatus.Dissolved); if (vacant) { // Vacant minipools waiting to be promoted need to be removed from the set maintained by the minipool manager RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); rocketMinipoolManager.removeVacantMinipool(); } else { if (depositType == MinipoolDeposit.Full) { // Handle legacy Full type minipool rocketMinipoolQueue.removeMinipool(MinipoolDeposit.Full); } else { // Transfer user balance (and penalty) to deposit pool uint256 userCapital = getUserDepositBalance(); rocketDepositPool.recycleDissolvedDeposit{value : userCapital + _penalty}(); // Emit ether withdrawn event emit EtherWithdrawn(address(rocketDepositPool), userCapital + _penalty, block.timestamp); } } } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolFactory.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; import "../RocketBase.sol"; import "../../interface/minipool/RocketMinipoolBaseInterface.sol"; import "../../interface/minipool/RocketMinipoolFactoryInterface.sol"; /// @notice Performs CREATE2 deployment of minipool contracts contract RocketMinipoolFactory is RocketBase, RocketMinipoolFactoryInterface { // Libs using SafeMath for uint; using Clones for address; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Returns the expected minipool address for a node operator given a user-defined salt /// @param _salt The salt used in minipool creation function getExpectedAddress(address _nodeOperator, uint256 _salt) external override view returns (address) { // Ensure rocketMinipoolBase is setAddress address rocketMinipoolBase = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", "rocketMinipoolBase"))); // Calculate node specific salt value bytes32 salt = keccak256(abi.encodePacked(_nodeOperator, _salt)); // Return expected address return rocketMinipoolBase.predictDeterministicAddress(salt, address(this)); } /// @notice Performs a CREATE2 deployment of a minipool contract with given salt /// @param _nodeAddress Owning node operator's address /// @param _salt A salt used in determining minipool address function deployContract(address _nodeAddress, uint256 _salt) override external onlyLatestContract("rocketMinipoolFactory", address(this)) onlyLatestContract("rocketMinipoolManager", msg.sender) returns (address) { // Ensure rocketMinipoolBase is setAddress address rocketMinipoolBase = rocketStorage.getAddress(keccak256(abi.encodePacked("contract.address", "rocketMinipoolBase"))); require(rocketMinipoolBase != address(0)); // Construct final salt bytes32 salt = keccak256(abi.encodePacked(_nodeAddress, _salt)); // Deploy the minipool address proxy = rocketMinipoolBase.cloneDeterministic(salt); // Initialise the minipool storage RocketMinipoolBaseInterface(proxy).initialise(address(rocketStorage), _nodeAddress); // Return address return proxy; } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolManager.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; pragma abicoder v2; import "../RocketBase.sol"; import "../../types/MinipoolStatus.sol"; import "../../types/MinipoolDeposit.sol"; import "../../types/MinipoolDetails.sol"; import "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../interface/minipool/RocketMinipoolInterface.sol"; import "../../interface/minipool/RocketMinipoolManagerInterface.sol"; import "../../interface/node/RocketNodeStakingInterface.sol"; import "../../interface/util/AddressSetStorageInterface.sol"; import "../../interface/node/RocketNodeManagerInterface.sol"; import "../../interface/network/RocketNetworkPricesInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import "../../interface/minipool/RocketMinipoolFactoryInterface.sol"; import "../../interface/node/RocketNodeDistributorFactoryInterface.sol"; import "../../interface/node/RocketNodeDistributorInterface.sol"; import "../../interface/network/RocketNetworkPenaltiesInterface.sol"; import "../../interface/minipool/RocketMinipoolPenaltyInterface.sol"; import "../../interface/node/RocketNodeDepositInterface.sol"; import "../network/RocketNetworkSnapshots.sol"; import "../node/RocketNodeStaking.sol"; /// @notice Minipool creation, removal and management contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface { // Events event MinipoolCreated(address indexed minipool, address indexed node, uint256 time); event MinipoolDestroyed(address indexed minipool, address indexed node, uint256 time); event BeginBondReduction(address indexed minipool, uint256 time); event CancelReductionVoted(address indexed minipool, address indexed member, uint256 time); event ReductionCancelled(address indexed minipool, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 6; } /// @notice Get the number of minipools in the network function getMinipoolCount() override public view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(bytes("minipools.index"))); } /// @notice Get the number of minipools in the network in the Staking state function getStakingMinipoolCount() override public view returns (uint256) { return getUint(keccak256(bytes("minipools.staking.count"))); } /// @notice Get the number of finalised minipools in the network function getFinalisedMinipoolCount() override external view returns (uint256) { return getUint(keccak256(bytes("minipools.finalised.count"))); } /// @notice Get the number of active minipools in the network function getActiveMinipoolCount() override public view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); uint256 total = addressSetStorage.getCount(keccak256(bytes("minipools.index"))); uint256 finalised = getUint(keccak256(bytes("minipools.finalised.count"))); return total - finalised; } /// @notice Returns true if a minipool has had an RPL slashing function getMinipoolRPLSlashed(address _minipoolAddress) override external view returns (bool) { return getBool(keccak256(abi.encodePacked("minipool.rpl.slashed", _minipoolAddress))); } /// @notice Get the number of minipools in each status. /// Returns the counts for Initialised, Prelaunch, Staking, Withdrawable, and Dissolved in that order. /// @param _offset The offset into the minipool set to start /// @param _limit The maximum number of minipools to iterate function getMinipoolCountPerStatus(uint256 _offset, uint256 _limit) override external view returns (uint256 initialisedCount, uint256 prelaunchCount, uint256 stakingCount, uint256 withdrawableCount, uint256 dissolvedCount) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Precompute minipool key bytes32 minipoolKey = keccak256(abi.encodePacked("minipools.index")); // Iterate over the requested minipool range uint256 totalMinipools = getMinipoolCount(); uint256 max = _offset + _limit; if (max > totalMinipools || _limit == 0) { max = totalMinipools; } for (uint256 i = _offset; i < max; ++i) { // Get the minipool at index i RocketMinipoolInterface minipool = RocketMinipoolInterface(addressSetStorage.getItem(minipoolKey, i)); // Get the minipool's status, and update the appropriate counter MinipoolStatus status = minipool.getStatus(); if (status == MinipoolStatus.Initialised) { initialisedCount++; } else if (status == MinipoolStatus.Prelaunch) { prelaunchCount++; } else if (status == MinipoolStatus.Staking) { stakingCount++; } else if (status == MinipoolStatus.Withdrawable) { withdrawableCount++; } else if (status == MinipoolStatus.Dissolved) { dissolvedCount++; } } } /// @notice Returns an array of all minipools in the prelaunch state /// @param _offset The offset into the minipool set to start iterating /// @param _limit The maximum number of minipools to iterate over function getPrelaunchMinipools(uint256 _offset, uint256 _limit) override external view returns (address[] memory) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Precompute minipool key bytes32 minipoolKey = keccak256(abi.encodePacked("minipools.index")); // Iterate over the requested minipool range uint256 totalMinipools = getMinipoolCount(); uint256 max = _offset + _limit; if (max > totalMinipools || _limit == 0) { max = totalMinipools; } // Create array big enough for every minipool address[] memory minipools = new address[](max - _offset); uint256 total = 0; for (uint256 i = _offset; i < max; ++i) { // Get the minipool at index i RocketMinipoolInterface minipool = RocketMinipoolInterface(addressSetStorage.getItem(minipoolKey, i)); // Get the minipool's status, and to array if it's in prelaunch MinipoolStatus status = minipool.getStatus(); if (status == MinipoolStatus.Prelaunch) { minipools[total] = address(minipool); total++; } } // Dirty hack to cut unused elements off end of return value assembly { mstore(minipools, total) } return minipools; } /// @notice Get a network minipool address by index /// @param _index Index into the minipool set to return function getMinipoolAt(uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked("minipools.index")), _index); } /// @notice Get the number of minipools owned by a node /// @param _nodeAddress The node operator to query the count of minipools of function getNodeMinipoolCount(address _nodeAddress) override external view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked("node.minipools.index", _nodeAddress))); } /// @notice Get the number of minipools owned by a node that are not finalised /// @param _nodeAddress The node operator to query the count of active minipools of function getNodeActiveMinipoolCount(address _nodeAddress) override public view returns (uint256) { bytes32 key = keccak256(abi.encodePacked("minipools.active.count", _nodeAddress)); RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); (bool exists,, uint224 count) = rocketNetworkSnapshots.latest(key); if (!exists){ // Fallback to old value AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); uint256 finalised = getUint(keccak256(abi.encodePacked("node.minipools.finalised.count", _nodeAddress))); uint256 total = addressSetStorage.getCount(keccak256(abi.encodePacked("node.minipools.index", _nodeAddress))); return total - finalised; } return uint256(count); } /// @notice Get the number of minipools owned by a node that are finalised /// @param _nodeAddress The node operator to query the count of finalised minipools of function getNodeFinalisedMinipoolCount(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.minipools.finalised.count", _nodeAddress))); } /// @notice Get the number of minipools owned by a node that are in staking status /// @param _nodeAddress The node operator to query the count of staking minipools of function getNodeStakingMinipoolCount(address _nodeAddress) override public view returns (uint256) { // Get valid deposit amounts uint256[2] memory depositSizes; depositSizes[0] = 16 ether; depositSizes[1] = 8 ether; uint256 total; for (uint256 i = 0; i < depositSizes.length; ++i){ total = total + getNodeStakingMinipoolCountBySize(_nodeAddress, depositSizes[i]); } return total; } /// @notice Get the number of minipools owned by a node that are in staking status /// @param _nodeAddress The node operator to query the count of minipools by desposit size of /// @param _depositSize The deposit size to filter result by function getNodeStakingMinipoolCountBySize(address _nodeAddress, uint256 _depositSize) override public view returns (uint256) { bytes32 nodeKey; if (_depositSize == 16 ether){ nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress)); } else { nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress, _depositSize)); } return getUint(nodeKey); } /// @notice Get a node minipool address by index /// @param _nodeAddress The node operator to query the minipool of /// @param _index Index into the node operator's set of minipools function getNodeMinipoolAt(address _nodeAddress, uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked("node.minipools.index", _nodeAddress)), _index); } /// @notice Get the number of validating minipools owned by a node /// @param _nodeAddress The node operator to query the count of validating minipools of function getNodeValidatingMinipoolCount(address _nodeAddress) override external view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked("node.minipools.validating.index", _nodeAddress))); } /// @notice Get a validating node minipool address by index /// @param _nodeAddress The node operator to query the validating minipool of /// @param _index Index into the node operator's set of validating minipools function getNodeValidatingMinipoolAt(address _nodeAddress, uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked("node.minipools.validating.index", _nodeAddress)), _index); } /// @notice Get a minipool address by validator pubkey /// @param _pubkey The pubkey to query function getMinipoolByPubkey(bytes memory _pubkey) override public view returns (address) { return getAddress(keccak256(abi.encodePacked("validator.minipool", _pubkey))); } /// @notice Returns true if a minipool exists /// @param _minipoolAddress The address of the minipool to check the existence of function getMinipoolExists(address _minipoolAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("minipool.exists", _minipoolAddress))); } /// @notice Returns true if a minipool previously existed at the given address /// @param _minipoolAddress The address to check the previous existence of a minipool at function getMinipoolDestroyed(address _minipoolAddress) override external view returns (bool) { return getBool(keccak256(abi.encodePacked("minipool.destroyed", _minipoolAddress))); } /// @notice Returns a minipool's validator pubkey /// @param _minipoolAddress The minipool to query the pubkey of function getMinipoolPubkey(address _minipoolAddress) override public view returns (bytes memory) { return getBytes(keccak256(abi.encodePacked("minipool.pubkey", _minipoolAddress))); } /// @notice Calculates what the withdrawal credentials of a minipool should be set to /// @param _minipoolAddress The minipool to calculate the withdrawal credentials for function getMinipoolWithdrawalCredentials(address _minipoolAddress) override public pure returns (bytes memory) { return abi.encodePacked(bytes1(0x01), bytes11(0x0), address(_minipoolAddress)); } /// @notice Decrements a node operator's number of staking minipools based on the minipools prior bond amount and /// increments it based on their new bond amount. /// @param _previousBond The minipool's previous bond value /// @param _newBond The minipool's new bond value /// @param _previousFee The fee of the minipool prior to the bond change /// @param _newFee The fee of the minipool after the bond change function updateNodeStakingMinipoolCount(uint256 _previousBond, uint256 _newBond, uint256 _previousFee, uint256 _newFee) override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { bytes32 nodeKey; bytes32 numeratorKey; // Get contracts RocketMinipoolInterface minipool = RocketMinipoolInterface(msg.sender); address nodeAddress = minipool.getNodeAddress(); // Try to distribute current fees at previous average commission rate _tryDistribute(nodeAddress); // Decrement previous bond count if (_previousBond == 16 ether){ nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", nodeAddress)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", nodeAddress)); } else { nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", nodeAddress, _previousBond)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", nodeAddress, _previousBond)); } subUint(nodeKey, 1); subUint(numeratorKey, _previousFee); // Increment new bond count if (_newBond == 16 ether){ nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", nodeAddress)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", nodeAddress)); } else { nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", nodeAddress, _newBond)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", nodeAddress, _newBond)); } addUint(nodeKey, 1); addUint(numeratorKey, _newFee); } /// @dev Increments a node operator's number of staking minipools and calculates updated average node fee. /// Must be called from the minipool itself as msg.sender is used to query the minipool's node fee /// @param _nodeAddress The node address to increment the number of staking minipools of function incrementNodeStakingMinipoolCount(address _nodeAddress) override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { // Get contracts RocketMinipoolInterface minipool = RocketMinipoolInterface(msg.sender); // Try to distribute current fees at previous average commission rate _tryDistribute(_nodeAddress); // Update the node specific count uint256 depositSize = minipool.getNodeDepositBalance(); bytes32 nodeKey; bytes32 numeratorKey; if (depositSize == 16 ether){ nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress)); } else { nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress, depositSize)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress, depositSize)); } uint256 nodeValue = getUint(nodeKey); setUint(nodeKey, nodeValue + 1); // Update the total count bytes32 totalKey = keccak256(abi.encodePacked("minipools.staking.count")); uint256 totalValue = getUint(totalKey); setUint(totalKey, totalValue + 1); // Update node fee average addUint(numeratorKey, minipool.getNodeFee()); } /// @dev Decrements a node operator's number of minipools in staking status and calculates updated average node fee. /// Must be called from the minipool itself as msg.sender is used to query the minipool's node fee /// @param _nodeAddress The node address to decrement the number of staking minipools of function decrementNodeStakingMinipoolCount(address _nodeAddress) override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { // Get contracts RocketMinipoolInterface minipool = RocketMinipoolInterface(msg.sender); // Try to distribute current fees at previous average commission rate _tryDistribute(_nodeAddress); // Update the node specific count uint256 depositSize = minipool.getNodeDepositBalance(); bytes32 nodeKey; bytes32 numeratorKey; if (depositSize == 16 ether){ nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress)); } else { nodeKey = keccak256(abi.encodePacked("node.minipools.staking.count", _nodeAddress, depositSize)); numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress, depositSize)); } uint256 nodeValue = getUint(nodeKey); setUint(nodeKey, nodeValue - 1); // Update the total count bytes32 totalKey = keccak256(abi.encodePacked("minipools.staking.count")); uint256 totalValue = getUint(totalKey); setUint(totalKey, totalValue - 1); // Update node fee average subUint(numeratorKey, minipool.getNodeFee()); } /// @notice Calls distribute on the given node's distributor if it has a balance and has been initialised /// @dev Reverts if node has not initialised their distributor /// @param _nodeAddress The node operator to try distribute rewards for function tryDistribute(address _nodeAddress) override external { _tryDistribute(_nodeAddress); } /// @dev Calls distribute on the given node's distributor if it has a balance and has been initialised /// @param _nodeAddress The node operator to try distribute rewards for function _tryDistribute(address _nodeAddress) internal { // Get contracts RocketNodeDistributorFactoryInterface rocketNodeDistributorFactory = RocketNodeDistributorFactoryInterface(getContractAddress("rocketNodeDistributorFactory")); address distributorAddress = rocketNodeDistributorFactory.getProxyAddress(_nodeAddress); // If there are funds to distribute than call distribute if (distributorAddress.balance > 0) { // Get contracts RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); // Ensure distributor has been initialised require(rocketNodeManager.getFeeDistributorInitialised(_nodeAddress), "Distributor not initialised"); RocketNodeDistributorInterface distributor = RocketNodeDistributorInterface(distributorAddress); distributor.distribute(); } } /// @dev Increments a node operator's number of minipools that have been finalised /// @param _nodeAddress The node operator to increment finalised minipool count for function incrementNodeFinalisedMinipoolCount(address _nodeAddress) override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { // Get active minipool count (before increasing finalised count in case of fallback calculation) uint256 activeMinipoolCount = getNodeActiveMinipoolCount(_nodeAddress); // Can only finalise a minipool once bytes32 finalisedKey = keccak256(abi.encodePacked("node.minipools.finalised", msg.sender)); require(!getBool(finalisedKey), "Minipool has already been finalised"); setBool(finalisedKey, true); // Update the node specific count addUint(keccak256(abi.encodePacked("node.minipools.finalised.count", _nodeAddress)), 1); // Update the total count addUint(keccak256(bytes("minipools.finalised.count")), 1); // Update ETH matched RocketNetworkSnapshots rocketNetworkSnapshots = RocketNetworkSnapshots(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("eth.matched.node.amount", _nodeAddress)); uint256 ethMatched = rocketNetworkSnapshots.latestValue(key); ethMatched -= RocketMinipoolInterface(msg.sender).getUserDepositBalance(); rocketNetworkSnapshots.push(key, uint224(ethMatched)); // Decrement active count key = keccak256(abi.encodePacked("minipools.active.count", _nodeAddress)); rocketNetworkSnapshots.push(key, uint224(activeMinipoolCount - 1)); } /// @dev Create a minipool. Only accepts calls from the RocketNodeDeposit contract /// @param _nodeAddress The owning node operator's address /// @param _salt A salt used in determining the minipool's address function createMinipool(address _nodeAddress, uint256 _salt) override public onlyLatestContract("rocketMinipoolManager", address(this)) onlyLatestContract("rocketNodeDeposit", msg.sender) returns (RocketMinipoolInterface) { // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Check node minipool limit based on RPL stake { // Local scope to prevent stack too deep error RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Check global minipool limit uint256 totalActiveMinipoolCount = getActiveMinipoolCount(); require(totalActiveMinipoolCount + 1 <= rocketDAOProtocolSettingsMinipool.getMaximumCount(), "Global minipool limit reached"); } // Get current active minipool count for this node operator (before we insert into address set in case it uses fallback calc) uint256 activeMinipoolCount = getNodeActiveMinipoolCount(_nodeAddress); // Create minipool contract address contractAddress = _deployContract(_nodeAddress, _salt); // Initialise minipool data setBool(keccak256(abi.encodePacked("minipool.exists", contractAddress)), true); // Add minipool to indexes addressSetStorage.addItem(keccak256(abi.encodePacked("minipools.index")), contractAddress); addressSetStorage.addItem(keccak256(abi.encodePacked("node.minipools.index", _nodeAddress)), contractAddress); // Increment active count RocketNetworkSnapshots rocketNetworkSnapshots = RocketNetworkSnapshots(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("minipools.active.count", _nodeAddress)); rocketNetworkSnapshots.push(key, uint224(activeMinipoolCount + 1)); // Emit minipool created event emit MinipoolCreated(contractAddress, _nodeAddress, block.timestamp); // Return created minipool address return RocketMinipoolInterface(contractAddress); } /// @notice Creates a vacant minipool that can be promoted by changing the given validator's withdrawal credentials /// @param _nodeAddress Address of the owning node operator /// @param _salt A salt used in determining the minipool's address /// @param _validatorPubkey A validator pubkey that the node operator intends to migrate the withdrawal credentials of /// @param _bondAmount The bond amount selected by the node operator /// @param _currentBalance The current balance of the validator on the beaconchain (will be checked by oDAO and scrubbed if not correct) function createVacantMinipool(address _nodeAddress, uint256 _salt, bytes calldata _validatorPubkey, uint256 _bondAmount, uint256 _currentBalance) override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyLatestContract("rocketNodeDeposit", msg.sender) returns (RocketMinipoolInterface) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Create the minipool RocketMinipoolInterface minipool = createMinipool(_nodeAddress, _salt); // Prepare the minipool minipool.prepareVacancy(_bondAmount, _currentBalance); // Set the minipool's validator pubkey _setMinipoolPubkey(address(minipool), _validatorPubkey); // Add minipool to the vacant set addressSetStorage.addItem(keccak256(abi.encodePacked("minipools.vacant.index")), address(minipool)); // Return return minipool; } /// @dev Called by minipool to remove from vacant set on promotion or dissolution function removeVacantMinipool() override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { // Remove from vacant set AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); addressSetStorage.removeItem(keccak256(abi.encodePacked("minipools.vacant.index")), msg.sender); // If minipool was dissolved, remove mapping of pubkey to minipool to allow NO to try again in future RocketMinipoolInterface minipool = RocketMinipoolInterface(msg.sender); if (minipool.getStatus() == MinipoolStatus.Dissolved) { bytes memory pubkey = getMinipoolPubkey(msg.sender); deleteAddress(keccak256(abi.encodePacked("validator.minipool", pubkey))); } } /// @notice Returns the number of minipools in the vacant minipool set function getVacantMinipoolCount() override external view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked("minipools.vacant.index"))); } /// @notice Returns the vacant minipool at a given index /// @param _index The index into the vacant minipool set to retrieve function getVacantMinipoolAt(uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked("minipools.vacant.index")), _index); } /// @dev Destroy a minipool cleaning up all relevant state. Only accepts calls from registered minipools function destroyMinipool() override external onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Initialize minipool & get properties RocketMinipoolInterface minipool = RocketMinipoolInterface(msg.sender); address nodeAddress = minipool.getNodeAddress(); // Update ETH matched RocketNetworkSnapshots rocketNetworkSnapshots = RocketNetworkSnapshots(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("eth.matched.node.amount", nodeAddress)); uint256 ethMatched = uint256(rocketNetworkSnapshots.latestValue(key)); ethMatched -= minipool.getUserDepositBalance(); rocketNetworkSnapshots.push(key, uint224(ethMatched)); // Update minipool data setBool(keccak256(abi.encodePacked("minipool.exists", msg.sender)), false); // Record minipool as destroyed to prevent recreation at same address setBool(keccak256(abi.encodePacked("minipool.destroyed", msg.sender)), true); // Get number of active minipools (before removing from address set in case of fallback calculation) uint256 activeMinipoolCount = getNodeActiveMinipoolCount(nodeAddress); // Remove minipool from indexes addressSetStorage.removeItem(keccak256(abi.encodePacked("minipools.index")), msg.sender); addressSetStorage.removeItem(keccak256(abi.encodePacked("node.minipools.index", nodeAddress)), msg.sender); // Clean up pubkey state bytes memory pubkey = getMinipoolPubkey(msg.sender); deleteBytes(keccak256(abi.encodePacked("minipool.pubkey", msg.sender))); deleteAddress(keccak256(abi.encodePacked("validator.minipool", pubkey))); // Decrement active count key = keccak256(abi.encodePacked("minipools.active.count", nodeAddress)); rocketNetworkSnapshots.push(key, uint224(activeMinipoolCount - 1)); // Emit minipool destroyed event emit MinipoolDestroyed(msg.sender, nodeAddress, block.timestamp); } /// @dev Set a minipool's validator pubkey. Only accepts calls from registered minipools /// @param _pubkey The pubkey to set for the calling minipool function setMinipoolPubkey(bytes calldata _pubkey) override public onlyLatestContract("rocketMinipoolManager", address(this)) onlyRegisteredMinipool(msg.sender) { _setMinipoolPubkey(msg.sender, _pubkey); } /// @dev Internal logic to set a minipool's pubkey, reverts if pubkey already set /// @param _pubkey The pubkey to set for the calling minipool function _setMinipoolPubkey(address _minipool, bytes calldata _pubkey) internal { // Check validator pubkey is not in use require(getMinipoolByPubkey(_pubkey) == address(0x0), "Validator pubkey is in use"); // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Initialise minipool & get properties RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipool); address nodeAddress = minipool.getNodeAddress(); // Set minipool validator pubkey & validator minipool address setBytes(keccak256(abi.encodePacked("minipool.pubkey", _minipool)), _pubkey); setAddress(keccak256(abi.encodePacked("validator.minipool", _pubkey)), _minipool); // Add minipool to node validating minipools index addressSetStorage.addItem(keccak256(abi.encodePacked("node.minipools.validating.index", nodeAddress)), _minipool); } /// @dev Wrapper around minipool getDepositType which handles backwards compatibility with v1 and v2 delegates /// @param _minipoolAddress Minipool address to get the deposit type of function getMinipoolDepositType(address _minipoolAddress) external override view returns (MinipoolDeposit) { RocketMinipoolInterface minipoolInterface = RocketMinipoolInterface(_minipoolAddress); uint8 version = 1; // Version 1 minipools did not have a version() function try minipoolInterface.version() returns (uint8 tryVersion) { version = tryVersion; } catch (bytes memory /*lowLevelData*/) {} if (version == 1 || version == 2) { try minipoolInterface.getDepositType{gas: 30000}() returns (MinipoolDeposit depositType) { return depositType; } catch (bytes memory /*lowLevelData*/) { return MinipoolDeposit.Variable; } } return minipoolInterface.getDepositType(); } /// @dev Performs a CREATE2 deployment of a minipool contract with given salt /// @param _nodeAddress The owning node operator's address /// @param _salt A salt used in determining the minipool's address function _deployContract(address _nodeAddress, uint256 _salt) internal returns (address) { RocketMinipoolFactoryInterface rocketMinipoolFactory = RocketMinipoolFactoryInterface(getContractAddress("rocketMinipoolFactory")); return rocketMinipoolFactory.deployContract(_nodeAddress, _salt); } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolPenalty.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "@openzeppelin/contracts/math/SafeMath.sol"; import "../RocketBase.sol"; import "../../interface/minipool/RocketMinipoolPenaltyInterface.sol"; // Non-upgradable contract which gives guardian control over maximum penalty rates contract RocketMinipoolPenalty is RocketBase, RocketMinipoolPenaltyInterface { // Events event MaxPenaltyRateUpdated(uint256 rate, uint256 time); // Libs using SafeMath for uint; // Storage (purposefully does not use RocketStorage to prevent oDAO from having power over this feature) uint256 maxPenaltyRate = 0 ether; // The most the oDAO is allowed to penalty a minipool (as a percentage) // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { } // Get/set the current max penalty rate function setMaxPenaltyRate(uint256 _rate) external override onlyGuardian { // Update rate maxPenaltyRate = _rate; // Emit event emit MaxPenaltyRateUpdated(_rate, block.timestamp); } function getMaxPenaltyRate() external override view returns (uint256) { return maxPenaltyRate; } // Retrieves the amount to penalty a minipool function getPenaltyRate(address _minipoolAddress) external override view returns(uint256) { // Quick out which avoids a call to RocketStorage if (maxPenaltyRate == 0) { return 0; } // Retrieve penalty rate for this minipool uint256 penaltyRate = getUint(keccak256(abi.encodePacked("minipool.penalty.rate", _minipoolAddress))); // min(maxPenaltyRate, penaltyRate) if (penaltyRate > maxPenaltyRate) { return maxPenaltyRate; } return penaltyRate; } // Sets the penalty rate for the given minipool function setPenaltyRate(address _minipoolAddress, uint256 _rate) external override onlyLatestNetworkContract { setUint(keccak256(abi.encodePacked("minipool.penalty.rate", _minipoolAddress)), _rate); } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolQueue.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/math/SignedSafeMath.sol"; import "@openzeppelin/contracts/utils/SafeCast.sol"; import "../RocketBase.sol"; import "../../interface/minipool/RocketMinipoolInterface.sol"; import "../../interface/minipool/RocketMinipoolQueueInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../interface/util/AddressQueueStorageInterface.sol"; import "../../types/MinipoolDeposit.sol"; /// @notice Minipool queueing for deposit assignment contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { // Libs using SafeMath for uint; using SignedSafeMath for int; // Constants bytes32 private constant queueKeyFull = keccak256("minipools.available.full"); bytes32 private constant queueKeyHalf = keccak256("minipools.available.half"); bytes32 private constant queueKeyVariable = keccak256("minipools.available.variable"); // Events event MinipoolEnqueued(address indexed minipool, bytes32 indexed queueId, uint256 time); event MinipoolDequeued(address indexed minipool, bytes32 indexed queueId, uint256 time); event MinipoolRemoved(address indexed minipool, bytes32 indexed queueId, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Get the total combined length of the queues function getTotalLength() override external view returns (uint256) { return ( getLengthLegacy(queueKeyFull) ).add( getLengthLegacy(queueKeyHalf) ).add( getLength() ); } /// @notice Returns true if there are any legacy minipools in the queue function getContainsLegacy() override external view returns (bool) { return getLengthLegacy(queueKeyFull).add(getLengthLegacy(queueKeyHalf)) > 0; } /// @notice Get the length of a given queue. Returns 0 for invalid queues /// @param _depositType Which queue to query the length of function getLengthLegacy(MinipoolDeposit _depositType) override external view returns (uint256) { if (_depositType == MinipoolDeposit.Full) { return getLengthLegacy(queueKeyFull); } if (_depositType == MinipoolDeposit.Half) { return getLengthLegacy(queueKeyHalf); } return 0; } /// @dev Returns a queue length by internal key representation /// @param _key The internal key representation of the queue to query the length of function getLengthLegacy(bytes32 _key) private view returns (uint256) { AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); return addressQueueStorage.getLength(_key); } /// @notice Gets the length of the variable (global) queue function getLength() override public view returns (uint256) { AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); return addressQueueStorage.getLength(queueKeyVariable); } /// @notice Get the total combined capacity of the queues function getTotalCapacity() override external view returns (uint256) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); return ( getLengthLegacy(queueKeyFull).mul(rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount()) ).add( getLengthLegacy(queueKeyHalf).mul(rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount()) ).add( getVariableCapacity() ); } /// @notice Get the total effective capacity of the queues (used in node demand calculation) function getEffectiveCapacity() override external view returns (uint256) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); return ( getLengthLegacy(queueKeyFull).mul(rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount()) ).add( getLengthLegacy(queueKeyHalf).mul(rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount()) ).add( getVariableCapacity() ); } /// @dev Get the ETH capacity of the variable queue function getVariableCapacity() internal view returns (uint256) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); return getLength().mul(rocketDAOProtocolSettingsMinipool.getVariableDepositAmount()); } /// @notice Get the capacity of the next available minipool. Returns 0 if no minipools are available function getNextCapacityLegacy() override external view returns (uint256) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); if (getLengthLegacy(queueKeyHalf) > 0) { return rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount(); } if (getLengthLegacy(queueKeyFull) > 0) { return rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount(); } return 0; } /// @notice Get the deposit type of the next available minipool and the number of deposits in that queue. /// Returns None if no minipools are available function getNextDepositLegacy() override external view returns (MinipoolDeposit, uint256) { uint256 length = getLengthLegacy(queueKeyHalf); if (length > 0) { return (MinipoolDeposit.Half, length); } length = getLengthLegacy(queueKeyFull); if (length > 0) { return (MinipoolDeposit.Full, length); } return (MinipoolDeposit.None, 0); } /// @dev Add a minipool to the end of the appropriate queue. Only accepts calls from the RocketMinipoolManager contract /// @param _minipool Address of the minipool to add to the queue function enqueueMinipool(address _minipool) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketNodeDeposit", msg.sender) { // Enqueue AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); addressQueueStorage.enqueueItem(queueKeyVariable, _minipool); // Emit enqueued event emit MinipoolEnqueued(_minipool, queueKeyVariable, block.timestamp); } /// @dev Dequeues a minipool from a legacy queue /// @param _depositType The queue to dequeue a minipool from function dequeueMinipoolByDepositLegacy(MinipoolDeposit _depositType) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) returns (address minipoolAddress) { if (_depositType == MinipoolDeposit.Half) { return dequeueMinipool(queueKeyHalf); } if (_depositType == MinipoolDeposit.Full) { return dequeueMinipool(queueKeyFull); } require(false, "No minipools are available"); } /// @dev Dequeues multiple minipools from the variable queue and returns them all /// @param _maxToDequeue The maximum number of items to dequeue function dequeueMinipools(uint256 _maxToDequeue) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) returns (address[] memory minipoolAddress) { uint256 queueLength = getLength(); uint256 count = _maxToDequeue; if (count > queueLength) { count = queueLength; } address[] memory minipoolAddresses = new address[](count); for (uint256 i = 0; i < count; i++) { RocketMinipoolInterface minipool = RocketMinipoolInterface(dequeueMinipool(queueKeyVariable)); minipoolAddresses[i] = address(minipool); } return minipoolAddresses; } /// @dev Dequeues a minipool from a queue given an internal key /// @param _key The internal key representation of the queue from which to dequeue a minipool from function dequeueMinipool(bytes32 _key) private returns (address) { // Dequeue AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); address minipool = addressQueueStorage.dequeueItem(_key); // Emit dequeued event emit MinipoolDequeued(minipool, _key, block.timestamp); // Return return minipool; } /// @dev Remove a minipool from a queue. Only accepts calls from registered minipools function removeMinipool(MinipoolDeposit _depositType) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyRegisteredMinipool(msg.sender) { // Remove minipool from queue if (_depositType == MinipoolDeposit.Half) { return removeMinipool(queueKeyHalf, msg.sender); } if (_depositType == MinipoolDeposit.Full) { return removeMinipool(queueKeyFull, msg.sender); } if (_depositType == MinipoolDeposit.Variable) { return removeMinipool(queueKeyVariable, msg.sender); } require(false, "Invalid minipool deposit type"); } /// @dev Removes a minipool from a queue given an internal key /// @param _key The internal key representation of the queue from which to remove a minipool from /// @param _minipool The address of a minipool to remove from the specified queue function removeMinipool(bytes32 _key, address _minipool) private { // Remove AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); addressQueueStorage.removeItem(_key, _minipool); // Emit removed event emit MinipoolRemoved(_minipool, _key, block.timestamp); } /// @notice Returns the minipool address of the minipool in the global queue at a given index /// @param _index The index into the queue to retrieve function getMinipoolAt(uint256 _index) override external view returns(address) { AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); // Check if index is in the half queue uint256 halfLength = addressQueueStorage.getLength(queueKeyHalf); if (_index < halfLength) { return addressQueueStorage.getItem(queueKeyHalf, _index); } _index = _index.sub(halfLength); // Check if index is in the full queue uint256 fullLength = addressQueueStorage.getLength(queueKeyFull); if (_index < fullLength) { return addressQueueStorage.getItem(queueKeyFull, _index); } _index = _index.sub(fullLength); // Check if index is in the full queue uint256 variableLength = addressQueueStorage.getLength(queueKeyVariable); if (_index < variableLength) { return addressQueueStorage.getItem(queueKeyVariable, _index); } // Index is out of bounds return address(0); } /// @notice Returns the position a given minipool is in the queue /// @param _minipool The minipool to query the position of function getMinipoolPosition(address _minipool) override external view returns (int256) { AddressQueueStorageInterface addressQueueStorage = AddressQueueStorageInterface(getContractAddress("addressQueueStorage")); int256 position; // Check in half queue position = addressQueueStorage.getIndexOf(queueKeyHalf, _minipool); if (position != -1) { return position; } int256 offset = SafeCast.toInt256(addressQueueStorage.getLength(queueKeyHalf)); // Check in full queue position = addressQueueStorage.getIndexOf(queueKeyFull, _minipool); if (position != -1) { return offset.add(position); } offset = offset.add(SafeCast.toInt256(addressQueueStorage.getLength(queueKeyFull))); // Check in variable queue position = addressQueueStorage.getIndexOf(queueKeyVariable, _minipool); if (position != -1) { return offset.add(position); } // Isn't in the queue return -1; } } ================================================ FILE: contracts/contract/minipool/RocketMinipoolStorageLayout.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../../interface/RocketStorageInterface.sol"; import "../../types/MinipoolDeposit.sol"; import "../../types/MinipoolStatus.sol"; // The RocketMinipool contract storage layout, shared by RocketMinipoolDelegate // ****************************************************** // Note: This contract MUST NOT BE UPDATED after launch. // All deployed minipool contracts must maintain a // Consistent storage layout with RocketMinipoolDelegate. // ****************************************************** abstract contract RocketMinipoolStorageLayout { // Storage state enum enum StorageState { Undefined, Uninitialised, Initialised } // Main Rocket Pool storage contract RocketStorageInterface internal rocketStorage = RocketStorageInterface(0); // Status MinipoolStatus internal status; uint256 internal statusBlock; uint256 internal statusTime; uint256 internal withdrawalBlock; // Deposit type MinipoolDeposit internal depositType; // Node details address internal nodeAddress; uint256 internal nodeFee; uint256 internal nodeDepositBalance; bool internal nodeDepositAssigned; // NO LONGER IN USE uint256 internal nodeRefundBalance; uint256 internal nodeSlashBalance; // User deposit details uint256 internal userDepositBalanceLegacy; uint256 internal userDepositAssignedTime; // Upgrade options bool internal useLatestDelegate = false; address internal rocketMinipoolDelegate; address internal rocketMinipoolDelegatePrev; // Local copy of RETH address address internal rocketTokenRETH; // Local copy of penalty contract address internal rocketMinipoolPenalty; // Used to prevent direct access to delegate and prevent calling initialise more than once StorageState internal storageState = StorageState.Undefined; // Whether node operator has finalised the pool bool internal finalised; // Trusted member scrub votes mapping(address => bool) internal memberScrubVotes; uint256 internal totalScrubVotes; // Variable minipool uint256 internal preLaunchValue; uint256 internal userDepositBalance; // Vacant minipool bool internal vacant; uint256 internal preMigrationBalance; // User distribution bool internal userDistributed; uint256 internal userDistributeTime; } ================================================ FILE: contracts/contract/network/RocketNetworkBalances.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../RocketBase.sol"; import "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../interface/network/RocketNetworkBalancesInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; /// @notice Oracle contract for network balance data contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { // Events event BalancesSubmitted(address indexed from, uint256 block, uint256 slotTimestamp, uint256 totalEth, uint256 stakingEth, uint256 rethSupply, uint256 blockTimestamp); event BalancesUpdated(uint256 indexed block, uint256 slotTimestamp, uint256 totalEth, uint256 stakingEth, uint256 rethSupply, uint256 blockTimestamp); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 4; } /// @notice The block number which balances are current for function getBalancesBlock() override public view returns (uint256) { return getUint(keccak256("network.balances.updated.block")); } /// @notice Sets the block number which balances are current for function _setBalancesBlock(uint256 _value) internal { setUint(keccak256("network.balances.updated.block"), _value); } /// @notice Get the timestamp for the last balance update function getBalancesTimestamp() override public view returns (uint256) { return getUint(keccak256("network.balances.updated.timestamp")); } /// @notice Sets the timestamp of the last balance update function _setBalancesTimestamp(uint256 _value) internal { setUint(keccak256("network.balances.updated.timestamp"), _value); } /// @notice The current RP network total ETH balance function getTotalETHBalance() override public view returns (uint256) { return getUint(keccak256("network.balance.total")); } /// @notice Sets the current RP network total ETH balance function _setTotalETHBalance(uint256 _value) internal { setUint(keccak256("network.balance.total"), _value); } /// @notice The current RP network staking ETH balance function getStakingETHBalance() override public view returns (uint256) { return getUint(keccak256("network.balance.staking")); } /// @notice Sets the current RP network staking ETH balance function _setStakingETHBalance(uint256 _value) internal { setUint(keccak256("network.balance.staking"), _value); } /// @notice The current RP network total rETH supply function getTotalRETHSupply() override public view returns (uint256) { return getUint(keccak256("network.balance.reth.supply")); } /// @notice Sets the current RP network total rETH supply function _setTotalRETHSupply(uint256 _value) internal { setUint(keccak256("network.balance.reth.supply"), _value); } /// @notice Get the current RP network ETH utilization rate as a fraction of 1 ETH /// Represents what % of the network's balance is actively earning rewards function getETHUtilizationRate() override external view returns (uint256) { uint256 totalEthBalance = getTotalETHBalance(); uint256 stakingEthBalance = getStakingETHBalance(); if (totalEthBalance == 0) { return calcBase; } return calcBase * stakingEthBalance / totalEthBalance; } /// @notice Submit network balances for a block. /// Only accepts calls from trusted (oracle) nodes. function submitBalances(uint256 _block, uint256 _slotTimestamp, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) onlyTrustedNode(msg.sender) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitBalancesEnabled(), "Submitting balances is currently disabled"); // Check block require(_block < block.number, "Balances can not be submitted for a future block"); uint256 lastBalancesBlock = getBalancesBlock(); require(_block >= lastBalancesBlock, "Network balances for a higher block are set"); // Check balances require(_stakingEth <= _totalEth, "Invalid network balances"); // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("network.balances.submitted.node", msg.sender, _block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply)); bytes32 submissionCountKey = keccak256(abi.encodePacked("network.balances.submitted.count", _block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply)); // Check & update node submission status require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encodePacked("network.balances.submitted.node", msg.sender, _block)), true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); // Emit balances submitted event emit BalancesSubmitted(msg.sender, _block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply, block.timestamp); // If voting past consensus, return if (_block == lastBalancesBlock) { return; } // Check submission count & update network balances RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); if (calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { _updateBalances(_block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply); } } /// @notice Executes updateBalances if consensus threshold is reached function executeUpdateBalances(uint256 _block, uint256 _slotTimestamp, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitBalancesEnabled(), "Submitting balances is currently disabled"); // Check block require(_block < block.number, "Balances can not be submitted for a future block"); require(_block > getBalancesBlock(), "Network balances for an equal or higher block are set"); // Check balances require(_stakingEth <= _totalEth, "Invalid network balances"); // Get submission keys bytes32 submissionCountKey = keccak256(abi.encodePacked("network.balances.submitted.count", _block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply)); // Get submission count uint256 submissionCount = getUint(submissionCountKey); // Check submission count & update network balances RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); require(calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold(), "Consensus has not been reached"); _updateBalances(_block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply); } /// @dev Internal method to update network balances function _updateBalances(uint256 _block, uint256 _slotTimestamp, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) internal { // Check enough time has passed (RPIP-61) RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); uint256 frequency = rocketDAOProtocolSettingsNetwork.getSubmitBalancesFrequency(); uint256 lastTimestamp = getBalancesTimestamp(); uint256 minimumTimestamp = lastTimestamp + (frequency * 95 / 100); require(block.timestamp >= minimumTimestamp, "Not enough time has passed"); _setBalancesTimestamp(block.timestamp); // Check rETH delta is within allowed range (RPIP-61) uint256 currentTotalEthBalance = getTotalETHBalance(); // Bypass the delta restriction on first balance update if (currentTotalEthBalance > 0) { uint256 currentRethSupply = getTotalRETHSupply(); uint256 currentRatio = calcBase * currentTotalEthBalance / currentRethSupply; uint256 newRatio = calcBase * _totalEth / _rethSupply; uint256 maxChangePercent = rocketDAOProtocolSettingsNetwork.getMaxRethDelta(); uint256 maxChange = currentRatio * maxChangePercent / calcBase; // Limit change per RPIP-61 if (newRatio > currentRatio) { require(newRatio - currentRatio <= maxChange, "Change exceeds maximum"); } else { require(currentRatio - newRatio <= maxChange, "Change exceeds maximum"); } } // Update balances _setBalancesBlock(_block); _setTotalETHBalance(_totalEth); _setStakingETHBalance(_stakingEth); _setTotalRETHSupply(_rethSupply); // Emit balances updated event emit BalancesUpdated(_block, _slotTimestamp, _totalEth, _stakingEth, _rethSupply, block.timestamp); } } ================================================ FILE: contracts/contract/network/RocketNetworkFees.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/SafeCast.sol"; import "../RocketBase.sol"; import "../../interface/deposit/RocketDepositPoolInterface.sol"; import "../../interface/minipool/RocketMinipoolQueueInterface.sol"; import "../../interface/network/RocketNetworkFeesInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; /// @notice Network node demand and commission rate contract RocketNetworkFees is RocketBase, RocketNetworkFeesInterface { // Libs using SafeMath for uint; using SafeCast for uint; constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Returns the current RP network node demand in ETH /// Node demand is equal to deposit pool balance minus available minipool capacity function getNodeDemand() override public view returns (int256) { // Load contracts RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); // Calculate & return int256 depositPoolBalance = rocketDepositPool.getBalance().toInt256(); int256 minipoolCapacity = rocketMinipoolQueue.getEffectiveCapacity().toInt256(); int256 demand = depositPoolBalance - minipoolCapacity; require(demand <= depositPoolBalance); return demand; } /// @notice Returns the current RP network node fee as a fraction of 1 ETH function getNodeFee() override external view returns (uint256) { return getNodeFeeByDemand(getNodeDemand()); } /// @notice Returns the network node fee for a given node demand value /// @param _nodeDemand The node demand to calculate the fee for function getNodeFeeByDemand(int256 _nodeDemand) override public view returns (uint256) { // Calculation base values uint256 demandDivisor = 1000000000000; // Get settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); uint256 minFee = rocketDAOProtocolSettingsNetwork.getMinimumNodeFee(); uint256 targetFee = rocketDAOProtocolSettingsNetwork.getTargetNodeFee(); uint256 maxFee = rocketDAOProtocolSettingsNetwork.getMaximumNodeFee(); uint256 demandRange = rocketDAOProtocolSettingsNetwork.getNodeFeeDemandRange(); // Normalize node demand uint256 nNodeDemand; bool nNodeDemandSign; if (_nodeDemand < 0) { nNodeDemand = uint256(-_nodeDemand); nNodeDemandSign = false; } else { nNodeDemand = uint256(_nodeDemand); nNodeDemandSign = true; } nNodeDemand = nNodeDemand.mul(calcBase).div(demandRange); // Check range bounds if (nNodeDemand == 0) { return targetFee; } if (nNodeDemand >= calcBase) { if (nNodeDemandSign) { return maxFee; } return minFee; } // Get fee interpolation factor uint256 t = nNodeDemand.div(demandDivisor) ** 3; // Interpolate between min / target / max fee if (nNodeDemandSign) { return targetFee.add(maxFee.sub(targetFee).mul(t).div(calcBase)); } return minFee.add(targetFee.sub(minFee).mul(calcBase.sub(t)).div(calcBase)); } } ================================================ FILE: contracts/contract/network/RocketNetworkPenalties.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketDAONodeTrustedInterface} from "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import {RocketDAOProtocolSettingsMinipoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketMinipoolPenaltyInterface} from "../../interface/minipool/RocketMinipoolPenaltyInterface.sol"; import {RocketNetworkPenaltiesInterface} from "../../interface/network/RocketNetworkPenaltiesInterface.sol"; import {RocketNetworkSnapshotsTimeInterface} from "../../interface/network/RocketNetworkSnapshotsTimeInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketBase} from "../RocketBase.sol"; /// @notice Applies penalties to minipools for MEV theft contract RocketNetworkPenalties is RocketBase, RocketNetworkPenaltiesInterface { // Constants uint256 constant internal penaltyMaximumPeriod = 7 days; bytes32 constant internal penaltyKey = keccak256(abi.encodePacked("minipool.running.penalty")); // Events event PenaltySubmitted(address indexed from, address indexed minipool, uint256 block, uint256 time); event PenaltyApplied(address indexed minipool, uint256 block, uint256 time); event PenaltyUpdated(address indexed minipool, uint256 penalty, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 2; } /// @notice Returns the number of votes in favour of the given penalty /// @param _minipool Address of the accused minipool /// @param _block Block that the theft occurred (used for uniqueness) function getVoteCount(address _minipool, uint256 _block) override external view returns (uint256) { bytes32 submissionCountKey = keccak256(abi.encodePacked("minipool.penalty.submission", _minipool, _block)); return getUint(submissionCountKey); } /// @notice Votes to penalise a minipool for MEV theft (only callable by oDAO) /// @param _minipool Address of the accused minipool /// @param _block Block that the theft occurred (used for uniqueness) function submitPenalty(address _minipool, uint256 _block) override external onlyTrustedNode(msg.sender) onlyRegisteredMinipool(_minipool) { require(_block < block.number, "Invalid block number"); // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("minipool.penalty.submission", msg.sender, _minipool, _block)); bytes32 submissionCountKey = keccak256(abi.encodePacked("minipool.penalty.submission", _minipool, _block)); // Check & update node submission status require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); // Maybe execute _maybeApplyPenalty(_minipool, _block, submissionCount); // Emit event emit PenaltySubmitted(msg.sender, _minipool, _block, block.timestamp); } /// @notice Manually execute a penalty that has hit majority vote /// @param _minipool Address of the accused minipool /// @param _block Block that the theft occurred (used for uniqueness) function executeUpdatePenalty(address _minipool, uint256 _block) override external { // Get submission count bytes32 submissionCountKey = keccak256(abi.encodePacked("minipool.penalty.submission", _minipool, _block)); uint256 submissionCount = getUint(submissionCountKey); // Apply penalty if relevant conditions are met _maybeApplyPenalty(_minipool, _block, submissionCount); } /// @notice Returns the running total of penalties at a given timestamp /// @param _time The timestamp to compute running total for function getPenaltyRunningTotalAtTime(uint64 _time) override external view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); return rocketNetworkSnapshotsTime.lookup(penaltyKey, _time); } /// @notice Returns the running total of penalties at the current time function getCurrentPenaltyRunningTotal() override external view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); (,,uint192 value) = rocketNetworkSnapshotsTime.latest(penaltyKey); return uint256(value); } /// @notice Returns the current maximum penalty based on the running total limitation function getCurrentMaxPenalty() override external view returns (uint256) { // Get contracts RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Grab max weekly penalty uint256 maxPenalty = rocketDAOProtocolSettingsMinipool.getMaximumPenaltyCount(); // Get running total from 7 days ago uint256 earlierTime = 0; if (block.timestamp > penaltyMaximumPeriod) { earlierTime = block.timestamp - penaltyMaximumPeriod; } uint256 earlierRunningTotal = uint256(rocketNetworkSnapshotsTime.lookup(penaltyKey, SafeCast.toUint64(earlierTime))); // Get current running total (,, uint192 currentRunningTotal) = rocketNetworkSnapshotsTime.latest(penaltyKey); // Cap the penalty at the maximum amount based on past 7 days uint256 currentTotal = uint256(currentRunningTotal) - earlierRunningTotal; if (currentTotal > maxPenalty) return 0; return maxPenalty - currentTotal; } /// @dev If a penalty has not been applied and hit majority, execute the penalty /// @param _minipool Address of the accused minipool /// @param _block Block that the theft occurred (used for uniqueness) function _maybeApplyPenalty(address _minipool, uint256 _block, uint256 _submissionCount) internal { // Check this penalty hasn't already reach majority and been applied bytes32 penaltyAppliedKey = keccak256(abi.encodePacked("minipool.penalty.submission.applied", _minipool, _block)); require(!getBool(penaltyAppliedKey), "Penalty already applied"); // Check for majority RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); if (calcBase * _submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodePenaltyThreshold()) { // Apply penalty and mark as applied setBool(penaltyAppliedKey, true); _applyPenalty(_minipool); // Emit event emit PenaltyApplied(_minipool, _block, block.timestamp); } } /// @dev Applies a penalty up to given amount, honouring the max penalty parameter function _applyPenalty(address _minipool) internal { // Get contracts RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Grab max weekly penalty uint256 maxPenalty = rocketDAOProtocolSettingsMinipool.getMaximumPenaltyCount(); // Get running total from 7 days ago uint256 earlierTime = 0; if (block.timestamp > penaltyMaximumPeriod) { earlierTime = block.timestamp - penaltyMaximumPeriod; } uint256 earlierRunningTotal = rocketNetworkSnapshotsTime.lookup(penaltyKey, SafeCast.toUint64(earlierTime)); // Get current running total (,, uint192 currentRunningTotal) = rocketNetworkSnapshotsTime.latest(penaltyKey); // Prevent the running penalty total from exceeding the maximum amount uint256 currentTotal = uint256(currentRunningTotal) - earlierRunningTotal; require(currentTotal < maxPenalty, "Max penalty exceeded"); uint256 currentMaxPenalty = maxPenalty - currentTotal; // Insert new running total rocketNetworkSnapshotsTime.push(penaltyKey, currentRunningTotal + 1); // Increment the penalty count on this minipool _incrementMinipoolPenaltyCount(_minipool); } /// @notice Returns the number of penalties for a given minipool /// @param _minipool Address of the minipool to query function getPenaltyCount(address _minipool) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("network.penalties.penalty", _minipool))); } /// @dev Increments the number of penalties against given minipool and updates penalty rate appropriately function _incrementMinipoolPenaltyCount(address _minipool) internal { // Get contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); // Calculate penalty count key bytes32 key = keccak256(abi.encodePacked("network.penalties.penalty", _minipool)); // Get the current penalty count uint256 newPenaltyCount = getUint(key) + 1; // Update the penalty count setUint(key, newPenaltyCount); // First two penalties do not increase penalty rate if (newPenaltyCount < 3) { return; } newPenaltyCount = newPenaltyCount - 2; // Calculate the new penalty rate uint256 penaltyRate = newPenaltyCount * rocketDAOProtocolSettingsNetwork.getPerPenaltyRate(); // Set the penalty rate RocketMinipoolPenaltyInterface rocketMinipoolPenalty = RocketMinipoolPenaltyInterface(getContractAddress("rocketMinipoolPenalty")); rocketMinipoolPenalty.setPenaltyRate(_minipool, penaltyRate); // Emit penalty updated event emit PenaltyUpdated(_minipool, penaltyRate, block.timestamp); } } ================================================ FILE: contracts/contract/network/RocketNetworkPrices.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "../RocketBase.sol"; import "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../interface/network/RocketNetworkPricesInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import "../../interface/network/RocketNetworkSnapshotsInterface.sol"; /// @notice Oracle contract for network token price data contract RocketNetworkPrices is RocketBase, RocketNetworkPricesInterface { // Constants bytes32 immutable priceKey; bytes32 immutable blockKey; // Events event PricesSubmitted(address indexed from, uint256 block, uint256 slotTimestamp, uint256 rplPrice, uint256 time); event PricesUpdated(uint256 indexed block, uint256 slotTimestamp, uint256 rplPrice, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Set contract version version = 4; // Precompute keys priceKey = keccak256("network.prices.rpl"); blockKey = keccak256("network.prices.updated.block"); } /// @notice Returns the block number which prices are current for function getPricesBlock() override public view returns (uint256) { return getUint(blockKey); } /// @dev Sets the block number which prices are current for function setPricesBlock(uint256 _value) private { setUint(blockKey, _value); } /// @notice Returns the current network RPL price in ETH function getRPLPrice() override public view returns (uint256) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); uint256 price = uint256(rocketNetworkSnapshots.latestValue(priceKey)); if (price == 0) { price = getUint(priceKey); } return price; } /// @dev Sets the current network RPL price in ETH function setRPLPrice(uint256 _value) private { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); rocketNetworkSnapshots.push(priceKey, uint224(_value)); } /// @notice Submit network price data for a block /// Only accepts calls from trusted (oracle) nodes /// @param _block The block this price submission is for /// @param _slotTimestamp timestamp for the slot the balance should be updated, which is used to find the latest valid EL block /// @param _rplPrice The price of RPL at the given block function submitPrices(uint256 _block, uint256 _slotTimestamp, uint256 _rplPrice) override external onlyLatestContract("rocketNetworkPrices", address(this)) onlyTrustedNode(msg.sender) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitPricesEnabled(), "Submitting prices is currently disabled"); // Check block require(_block < block.number, "Prices can not be submitted for a future block"); uint256 lastPricesBlock = getPricesBlock(); require(_block >= lastPricesBlock, "Network prices for a higher block are set"); // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("network.prices.submitted.node.key", msg.sender, _block, _slotTimestamp, _rplPrice)); bytes32 submissionCountKey = keccak256(abi.encodePacked("network.prices.submitted.count", _block, _slotTimestamp, _rplPrice)); // Check & update node submission status require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encodePacked("network.prices.submitted.node", msg.sender, _block)), true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); // Emit prices submitted event emit PricesSubmitted(msg.sender, _block, _slotTimestamp, _rplPrice, block.timestamp); // If voting past consensus, return if (_block == lastPricesBlock) { return; } // Check submission count & update network prices RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); if ((calcBase * submissionCount) / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { // Update the price updatePrices(_block, _slotTimestamp, _rplPrice); } } /// @notice Executes updatePrices if consensus threshold is reached /// @param _block The block to execute price update for /// @param _slotTimestamp timestamp for the slot the balance should be updated, which is used to find the latest valid EL block /// @param _rplPrice The price of RPL at the given block function executeUpdatePrices(uint256 _block, uint256 _slotTimestamp, uint256 _rplPrice) override external onlyLatestContract("rocketNetworkPrices", address(this)) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitPricesEnabled(), "Submitting prices is currently disabled"); // Check block require(_block < block.number, "Prices can not be submitted for a future block"); require(_block > getPricesBlock(), "Network prices for an equal or higher block are set"); // Get submission keys bytes32 submissionCountKey = keccak256(abi.encodePacked("network.prices.submitted.count", _block, _slotTimestamp, _rplPrice)); // Get submission count uint256 submissionCount = getUint(submissionCountKey); // Check submission count & update network prices RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); require((calcBase * submissionCount) / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold(), "Consensus has not been reached"); // Update the price updatePrices(_block, _slotTimestamp, _rplPrice); } /// @dev Update network price data /// @param _block The block to update price for /// @param _rplPrice The price of RPL at the given block function updatePrices(uint256 _block, uint256 _slotTimestamp, uint256 _rplPrice) private { // Update price setRPLPrice(_rplPrice); setPricesBlock(_block); // Emit prices updated event emit PricesUpdated(_block, _slotTimestamp, _rplPrice, block.timestamp); } } ================================================ FILE: contracts/contract/network/RocketNetworkRevenues.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketNetworkRevenuesInterface} from "../../interface/network/RocketNetworkRevenuesInterface.sol"; import {RocketNetworkSnapshotsTimeInterface} from "../../interface/network/RocketNetworkSnapshotsTimeInterface.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; /// @notice Handles the calculations of revenue splits for the protocol's Universal Adjustable Revenue Split contract RocketNetworkRevenues is RocketBase, RocketNetworkRevenuesInterface { // Constants uint256 private constant shareMagnitude = 100_000; uint256 private constant shareScale = 1 ether / shareMagnitude; bytes32 private constant nodeShareKey = keccak256(abi.encodePacked("network.revenue.node.share")); bytes32 private constant voterShareKey = keccak256(abi.encodePacked("network.revenue.voter.share")); bytes32 private constant protocolDAOShareKey = keccak256(abi.encodePacked("network.revenue.pdao.share")); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @dev Only allows calls from the pDAO setting contract or the security DAO contract modifier onlyProtocolOrSecurityDAO() { if(msg.sender != getAddress(keccak256(abi.encodePacked("contract.address", "rocketDAOProtocolSettingsNetwork")))) { if(msg.sender != getAddress(keccak256(abi.encodePacked("contract.address", "rocketDAOSecurityProposals")))) { revert("Invalid or outdated network contract"); } } _; } /// @dev Only allows calls from the pDAO setting contract modifier onlyProtocolDAO() { if(msg.sender != getAddress(keccak256(abi.encodePacked("contract.address", "rocketDAOProtocolSettingsNetwork")))) { revert("Invalid or outdated network contract"); } _; } /// @notice Used following an upgrade or new deployment to initialise the revenue split system /// @param _initialNodeShare The initial value to for the node share /// @param _initialVoterShare The initial value to for the voter share /// @param _initialProtocolDAOShare The initial value to for the pdao share function initialise(uint256 _initialNodeShare, uint256 _initialVoterShare, uint256 _initialProtocolDAOShare) override public { // On new deploy, allow guardian to initialise, otherwise, only a network contract if (rocketStorage.getDeployedStatus()) { require(getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))), "Invalid or outdated network contract"); } else { require(msg.sender == rocketStorage.getGuardian(), "Not guardian"); } // Initialise the shares RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); (bool exists,,) = rocketNetworkSnapshotsTime.latest(nodeShareKey); require(!exists, "Already initialised"); // Initialise node share bytes32 valueKey = bytes32(uint256(nodeShareKey) + block.timestamp); setUint(valueKey, _initialNodeShare / shareScale); rocketNetworkSnapshotsTime.push(nodeShareKey, 0); // Initialise voter share valueKey = bytes32(uint256(voterShareKey) + block.timestamp); setUint(valueKey, _initialVoterShare / shareScale); rocketNetworkSnapshotsTime.push(voterShareKey, 0); // Initialise pdao share valueKey = bytes32(uint256(protocolDAOShareKey) + block.timestamp); setUint(valueKey, _initialProtocolDAOShare / shareScale); rocketNetworkSnapshotsTime.push(protocolDAOShareKey, 0); } /// @notice Returns the current node share value function getCurrentNodeShare() external override view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); return _getCurrentShare(rocketNetworkSnapshotsTime, nodeShareKey, false); } /// @notice Returns the current voter share value function getCurrentVoterShare() external override view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); return _getCurrentShare(rocketNetworkSnapshotsTime, voterShareKey, false); } /// @notice Returns the current pDAO share value function getCurrentProtocolDAOShare() external override view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); return _getCurrentShare(rocketNetworkSnapshotsTime, protocolDAOShareKey, false); } /// @notice Called by a pDAO governance contract or security council to update the `node_operator_commission_share` parameter /// @param _newShare The value to set `node_operator_commission_share` to function setNodeShare(uint256 _newShare) external override onlyProtocolOrSecurityDAO { _setShare(nodeShareKey, _newShare); } /// @notice Called by a pDAO governance contract or security council to update the `voter_share` parameter /// @param _newShare The value to set the `voter_share` to function setVoterShare(uint256 _newShare) external override onlyProtocolOrSecurityDAO { _setShare(voterShareKey, _newShare); } /// @notice Called by a pDAO governance contract to update the `pdao_share` parameter /// @param _newShare The value to set the `pdao_share` to function setProtocolDAOShare(uint256 _newShare) external override onlyProtocolDAO { _setShare(protocolDAOShareKey, _newShare); } /// @notice Calculates the time-weighted average revenue split values between the supplied timestamp and now /// @param _sinceTime The starting block timestamp for the calculation function calculateSplit(uint64 _sinceTime) external override view returns (uint256 nodeShare, uint256 voterShare, uint256 protocolDAOShare, uint256 rethShare) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); if (_sinceTime == block.timestamp) { nodeShare = _getCurrentShare(rocketNetworkSnapshotsTime, nodeShareKey, true); voterShare = _getCurrentShare(rocketNetworkSnapshotsTime, voterShareKey, true); protocolDAOShare = _getCurrentShare(rocketNetworkSnapshotsTime, protocolDAOShareKey, true); } else { require(_sinceTime < block.timestamp, "Time must be in the past"); nodeShare = _getAverageSince(rocketNetworkSnapshotsTime, _sinceTime, nodeShareKey, true); voterShare = _getAverageSince(rocketNetworkSnapshotsTime, _sinceTime, voterShareKey, true); protocolDAOShare = _getAverageSince(rocketNetworkSnapshotsTime, _sinceTime, protocolDAOShareKey, true); } uint256 rethCommission = nodeShare + voterShare + protocolDAOShare; rethShare = 1 ether - rethCommission; return (nodeShare, voterShare, protocolDAOShare, rethShare); } /// @notice Called by a Megapool when its capital ratio changes to keep track of average /// @param _nodeAddress Address of the node operator /// @param _value New capital ratio function setNodeCapitalRatio(address _nodeAddress, uint256 _value) external override onlyRegisteredMegapool(msg.sender) onlyLatestContract("rocketNetworkRevenues", address(this)) { // Sanity check that ratio should be between 0 and 1 require(_value <= 1 ether, "Invalid capital ratio"); // Compute the key bytes32 key = keccak256(abi.encodePacked("node.capital.ratio", _nodeAddress)); // Get the existing value RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); uint256 storedValue = _getCurrentShare(rocketNetworkSnapshotsTime, key, false) / shareScale; // Don't store an entry if the capital ratio hasn't changed if (storedValue != _value / shareScale) { _setShare(key, _value); } } /// @notice Returns the current capital ratio of the given node operator /// @param _nodeAddress Address of the node operator to query the value for function getNodeCapitalRatio(address _nodeAddress) external override view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); bytes32 key = keccak256(abi.encodePacked("node.capital.ratio", _nodeAddress)); return _getCurrentShare(rocketNetworkSnapshotsTime, key, false); } /// @notice Returns the average capital ratio of the given node operator since a given timestamp /// @param _nodeAddress Address of the node operator to query the value for /// @param _sinceTime The timestamp to calculate the average since function getNodeAverageCapitalRatioSince(address _nodeAddress, uint64 _sinceTime) external override view returns (uint256) { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); bytes32 key = keccak256(abi.encodePacked("node.capital.ratio", _nodeAddress)); if (_sinceTime == block.timestamp) { return _getCurrentShare(rocketNetworkSnapshotsTime, key, true); } else { require(_sinceTime < block.timestamp, "Time must be in the past"); return _getAverageSince(rocketNetworkSnapshotsTime, _sinceTime, key, true); } } /// @notice Calculates the time-weighted average since a given block function _getAverageSince(RocketNetworkSnapshotsTimeInterface _rocketNetworkSnapshotsTime, uint64 _sinceTime, bytes32 _key, bool _mustExist) internal view returns (uint256) { (bool checkpointExists, uint64 checkpointTime, uint192 checkpointValue) = _rocketNetworkSnapshotsTime.latest(_key); require(!_mustExist || checkpointExists, "Snapshot does not exist"); if (!checkpointExists) return 0; if (checkpointTime <= _sinceTime) { // Value hasn't changed since _sinceTime, so return current bytes32 valueKey = bytes32(uint256(_key) + checkpointTime); return getUint(valueKey) * shareScale; } // Calculate the current accumulator value bytes32 valueKey = bytes32(uint256(_key) + checkpointTime); uint256 valueAtTime = getUint(valueKey); uint256 durationSinceCheckpoint = (block.timestamp - checkpointTime); uint256 currentAccum = uint256(checkpointValue) + (valueAtTime * durationSinceCheckpoint); // Calculate the accumulator at _sinceTime (checkpointExists, checkpointTime, checkpointValue) = _rocketNetworkSnapshotsTime.lookupCheckpoint(_key, SafeCast.toUint64(_sinceTime)); require(!_mustExist || checkpointExists, "Snapshot does not exist"); valueKey = bytes32(uint256(_key) + checkpointTime); valueAtTime = getUint(valueKey); durationSinceCheckpoint = (_sinceTime - checkpointTime); uint256 pastAccum = uint256(checkpointValue) + (valueAtTime * durationSinceCheckpoint); // Calculate time-weighted average uint256 durationSince = (block.timestamp - _sinceTime); uint256 average = (currentAccum - pastAccum) / durationSince; return average * shareScale; } /// @dev Calculates the cumulative value of the accumulator at a given timestamp function _getAccumulatorAt(RocketNetworkSnapshotsTimeInterface _rocketNetworkSnapshotsTime, bytes32 _key, uint256 _time, bool _mustExist) internal view returns (uint256) { (bool checkpointExists, uint64 checkpointTime, uint192 checkpointValue) = _rocketNetworkSnapshotsTime.lookupCheckpoint(_key, SafeCast.toUint64(_time)); require(!_mustExist || checkpointExists, "Snapshot does not exist"); if (!checkpointExists) return 0; bytes32 valueKey = bytes32(uint256(_key) + checkpointTime); uint256 valueAtTime = getUint(valueKey); uint256 timeDuration = (_time - checkpointTime); return uint256(checkpointValue) + (valueAtTime * timeDuration); } /// @dev Convenience method to return the current value given a key function _getCurrentShare(RocketNetworkSnapshotsTimeInterface _rocketNetworkSnapshotsTime, bytes32 _key, bool _mustExist) internal view returns (uint256) { (bool exists, uint64 timestamp, ) = _rocketNetworkSnapshotsTime.latest(_key); require(!_mustExist || exists, "Snapshot does not exist"); if (!exists) return 0; bytes32 valueKey = bytes32(uint256(_key) + timestamp); return getUint(valueKey) * shareScale; } /// @dev Sets the share value of the given key /// @param _key Key of the share value to set /// @param _newShare Value to set it to function _setShare(bytes32 _key, uint256 _newShare) internal { RocketNetworkSnapshotsTimeInterface rocketNetworkSnapshotsTime = RocketNetworkSnapshotsTimeInterface(getContractAddress("rocketNetworkSnapshotsTime")); uint256 currentAccum = _getAccumulatorAt(rocketNetworkSnapshotsTime, _key, block.timestamp, false); rocketNetworkSnapshotsTime.push(_key, SafeCast.toUint192(currentAccum)); uint256 newShareScaled = _newShare / shareScale; bytes32 valueKey = bytes32(uint256(_key) + block.timestamp); setUint(valueKey, newShareScaled); } } ================================================ FILE: contracts/contract/network/RocketNetworkSnapshots.sol ================================================ // SPDX-License-Identifier: MIT // Copyright (c) 2016-2023 zOS Global Limited and contributors // Adapted from OpenZeppelin `Checkpoints` contract pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {Math} from "@openzeppelin4/contracts/utils/math/Math.sol"; /// @notice Accounting for snapshotting of values based on block numbers contract RocketNetworkSnapshots is RocketBase, RocketNetworkSnapshotsInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Set contract version version = 2; // Setup for if this contract is being deployed as part of a new instance deployment if (!rocketStorage.getDeployedStatus()) { _insert(keccak256("network.prices.rpl"), 0.01 ether); _insert(keccak256("node.voting.power.stake.maximum"), 1.5 ether); } } /// @notice Pushes a new value as at the current block /// @param _key Key of the set to insert value into /// @param _value New value to insert function push(bytes32 _key, uint224 _value) onlyLatestContract("rocketNetworkSnapshots", address(this)) onlyLatestNetworkContract external { _insert(_key, _value); } /// @notice Returns the number of snapshots for a given key /// @param _key Key to query function length(bytes32 _key) public view returns (uint256) { return rocketStorage.getUint(keccak256(abi.encodePacked("snapshot.length", _key))); } /// @notice Returns the latest entry in a given set /// @param _key Key to query latest entry for function latest(bytes32 _key) external view returns (bool exists, uint32 block, uint224 value) { uint256 len = length(_key); if (len == 0) { return (false, 0, 0); } Checkpoint224 memory checkpoint = _load(_key, len - 1); return (true, checkpoint._block, checkpoint._value); } /// @notice Returns the block of the latest entry in a set /// @param _key Key to query latest block for function latestBlock(bytes32 _key) external view returns (uint32) { uint256 len = length(_key); return len == 0 ? 0 : _blockAt(_key, len - 1); } /// @notice Returns the value of the latest entry in a set /// @param _key Key to query latest value for function latestValue(bytes32 _key) external view returns (uint224) { uint256 len = length(_key); return len == 0 ? 0 : _valueAt(_key, len - 1); } /// @notice Performs a binary lookup for the value at the given block /// @param _key Key to execute lookup for /// @param _block Block number to search for function lookup(bytes32 _key, uint32 _block) external view returns (uint224) { uint256 len = length(_key); uint256 pos = _binaryLookup(_key, _block, 0, len); return pos == 0 ? 0 : _valueAt(_key, pos - 1); } /// @notice Performs a binary lookup for the entry at the given block /// @param _key Key to execute lookup for /// @param _block Block number to search for function lookupCheckpoint(bytes32 _key, uint32 _block) external override view returns (bool exists, uint32 block, uint224 value) { uint256 len = length(_key); uint256 pos = _binaryLookup(_key, _block, 0, len); if (pos == 0) { return (false, 0, 0); } Checkpoint224 memory checkpoint = _load(_key, pos - 1); return (true, checkpoint._block, checkpoint._value); } /// @notice Performs a binary lookup with a recency bias for the value at the given block /// @param _key Key to execute lookup for /// @param _block Block number to search for function lookupRecent(bytes32 _key, uint32 _block, uint256 _recency) external view returns (uint224) { uint256 len = length(_key); uint256 low = 0; uint256 high = len; if (len > 5 && len > _recency) { uint256 mid = len - _recency; if (_block < _blockAt(_key, mid)) { high = mid; } else { low = mid + 1; } } uint256 pos = _binaryLookup(_key, _block, low, high); return pos == 0 ? 0 : _valueAt(_key, pos - 1); } /// @dev Inserts a value into a snapshot set function _insert(bytes32 _key, uint224 _value) internal { uint32 blockNumber = SafeCast.toUint32(block.number); uint256 pos = length(_key); if (pos > 0) { Checkpoint224 memory last = _load(_key, pos - 1); // Checkpoint keys must be non-decreasing. require(last._block <= blockNumber, "Unordered snapshot insertion"); // Update or push new checkpoint if (last._block == blockNumber) { last._value = _value; _set(_key, pos - 1, last); } else { _push(_key, Checkpoint224({_block: blockNumber, _value: _value})); } } else { _push(_key, Checkpoint224({_block: blockNumber, _value: _value})); } } function _binaryLookup( bytes32 _key, uint32 _block, uint256 _low, uint256 _high ) internal view returns (uint256) { while (_low < _high) { uint256 mid = Math.average(_low, _high); if (_blockAt(_key, mid) > _block) { _high = mid; } else { _low = mid + 1; } } return _high; } /// @dev Loads and decodes a checkpoint entry function _load(bytes32 _key, uint256 _pos) internal view returns (Checkpoint224 memory) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); Checkpoint224 memory result; result._block = uint32(uint256(raw) >> 224); result._value = uint224(uint256(raw)); return result; } /// @dev Returns the block number of an entry at given position in the snapshot set function _blockAt(bytes32 _key, uint256 _pos) internal view returns (uint32) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); return uint32(uint256(raw) >> 224); } /// @dev Returns the value at given position in the snapshot set function _valueAt(bytes32 _key, uint256 _pos) internal view returns (uint224) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); return uint224(uint256(raw)); } /// @dev Pushes a new entry into the checkpoint set function _push(bytes32 _key, Checkpoint224 memory _item) internal { bytes32 lengthKey = keccak256(abi.encodePacked("snapshot.length", _key)); uint256 snapshotLength = rocketStorage.getUint(lengthKey); bytes32 key = bytes32(uint256(_key) + snapshotLength); rocketStorage.setUint(lengthKey, snapshotLength + 1); rocketStorage.setBytes32(key, _encode(_item)); } /// @dev Stores an entry into the checkpoint set function _set(bytes32 _key, uint256 _pos, Checkpoint224 memory _item) internal { bytes32 key = bytes32(uint256(_key) + _pos); rocketStorage.setBytes32(key, _encode(_item)); } /// @dev Encodes an entry into its 256 bit representation function _encode(Checkpoint224 memory _item) internal pure returns (bytes32) { return bytes32( uint256(_item._block) << 224 | uint256(_item._value) ); } } ================================================ FILE: contracts/contract/network/RocketNetworkSnapshotsTime.sol ================================================ // SPDX-License-Identifier: MIT // Copyright (c) 2016-2023 zOS Global Limited and contributors // Adapted from OpenZeppelin `Checkpoints` contract pragma solidity 0.8.30; import {SafeCast} from "@openzeppelin4/contracts/utils/math/SafeCast.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketNetworkSnapshotsTimeInterface} from "../../interface/network/RocketNetworkSnapshotsTimeInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {Math} from "@openzeppelin4/contracts/utils/math/Math.sol"; /// @notice Accounting for snapshotting of values based on block timestamps contract RocketNetworkSnapshotsTime is RocketBase, RocketNetworkSnapshotsTimeInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @notice Pushes a new value as at the current block timestamp /// @param _key Key of the set to insert value into /// @param _value New value to insert function push(bytes32 _key, uint192 _value) onlyLatestContract("rocketNetworkSnapshotsTime", address(this)) onlyLatestNetworkContract external { _insert(_key, _value); } /// @notice Returns the number of snapshots for a given key /// @param _key Key to query function length(bytes32 _key) public view returns (uint256) { return rocketStorage.getUint(keccak256(abi.encodePacked("snapshot.time.length", _key))); } /// @notice Returns the latest entry in a given set /// @param _key Key to query latest entry for function latest(bytes32 _key) external view returns (bool exists, uint64 time, uint192 value) { uint256 len = length(_key); if (len == 0) { return (false, 0, 0); } Checkpoint192 memory checkpoint = _load(_key, len - 1); return (true, checkpoint._time, checkpoint._value); } /// @notice Returns the timestamp of the latest entry in a set /// @param _key Key to query latest timestamp for function latestTime(bytes32 _key) external view returns (uint64) { uint256 len = length(_key); return len == 0 ? 0 : _timeAt(_key, len - 1); } /// @notice Returns the value of the latest entry in a set /// @param _key Key to query latest value for function latestValue(bytes32 _key) external view returns (uint192) { uint256 len = length(_key); return len == 0 ? 0 : _valueAt(_key, len - 1); } /// @notice Performs a binary lookup for the value at the given timestamp /// @param _key Key to execute lookup for /// @param _time Timestamp to search for function lookup(bytes32 _key, uint64 _time) external view returns (uint192) { uint256 len = length(_key); uint256 pos = _binaryLookup(_key, _time, 0, len); return pos == 0 ? 0 : _valueAt(_key, pos - 1); } /// @notice Performs a binary lookup for the entry at the given timestamp /// @param _key Key to execute lookup for /// @param _time Timestamp to search for function lookupCheckpoint(bytes32 _key, uint64 _time) external override view returns (bool exists, uint64 time, uint192 value) { uint256 len = length(_key); uint256 pos = _binaryLookup(_key, _time, 0, len); if (pos == 0) { return (false, 0, 0); } Checkpoint192 memory checkpoint = _load(_key, pos - 1); return (true, checkpoint._time, checkpoint._value); } /// @notice Performs a binary lookup with a recency bias for the value at the given timestamp /// @param _key Key to execute lookup for /// @param _time Timestamp to search for function lookupRecent(bytes32 _key, uint64 _time, uint256 _recency) external view returns (uint192) { uint256 len = length(_key); uint256 low = 0; uint256 high = len; if (len > 5 && len > _recency) { uint256 mid = len - _recency; if (_time < _timeAt(_key, mid)) { high = mid; } else { low = mid + 1; } } uint256 pos = _binaryLookup(_key, _time, low, high); return pos == 0 ? 0 : _valueAt(_key, pos - 1); } /// @dev Inserts a value into a snapshot set function _insert(bytes32 _key, uint192 _value) internal { uint64 blockTimestamp = SafeCast.toUint64(block.timestamp); uint256 pos = length(_key); if (pos > 0) { Checkpoint192 memory last = _load(_key, pos - 1); // Checkpoint keys must be non-decreasing. require(last._time <= blockTimestamp, "Unordered snapshot insertion"); // Update or push new checkpoint if (last._time == blockTimestamp) { last._value = _value; _set(_key, pos - 1, last); } else { _push(_key, Checkpoint192({_time: blockTimestamp, _value: _value})); } } else { _push(_key, Checkpoint192({_time: blockTimestamp, _value: _value})); } } function _binaryLookup( bytes32 _key, uint64 _time, uint256 _low, uint256 _high ) internal view returns (uint256) { while (_low < _high) { uint256 mid = Math.average(_low, _high); if (_timeAt(_key, mid) > _time) { _high = mid; } else { _low = mid + 1; } } return _high; } /// @dev Loads and decodes a checkpoint entry function _load(bytes32 _key, uint256 _pos) internal view returns (Checkpoint192 memory) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); Checkpoint192 memory result; result._time = uint64(uint256(raw) >> 192); result._value = uint192(uint256(raw)); return result; } /// @dev Returns the timestamp of an entry at given position in the snapshot set function _timeAt(bytes32 _key, uint256 _pos) internal view returns (uint64) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); return uint64(uint256(raw) >> 192); } /// @dev Returns the value at given position in the snapshot set function _valueAt(bytes32 _key, uint256 _pos) internal view returns (uint192) { bytes32 key = bytes32(uint256(_key) + _pos); bytes32 raw = rocketStorage.getBytes32(key); return uint192(uint256(raw)); } /// @dev Pushes a new entry into the checkpoint set function _push(bytes32 _key, Checkpoint192 memory _item) internal { bytes32 lengthKey = keccak256(abi.encodePacked("snapshot.time.length", _key)); uint256 snapshotLength = rocketStorage.getUint(lengthKey); bytes32 key = bytes32(uint256(_key) + snapshotLength); rocketStorage.setUint(lengthKey, snapshotLength + 1); rocketStorage.setBytes32(key, _encode(_item)); } /// @dev Stores an entry into the checkpoint set function _set(bytes32 _key, uint256 _pos, Checkpoint192 memory _item) internal { bytes32 key = bytes32(uint256(_key) + _pos); rocketStorage.setBytes32(key, _encode(_item)); } /// @dev Encodes an entry into its 256 bit representation function _encode(Checkpoint192 memory _item) internal pure returns (bytes32) { return bytes32( uint256(_item._time) << 192 | uint256(_item._value) ); } } ================================================ FILE: contracts/contract/network/RocketNetworkVoting.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import "@openzeppelin4/contracts/utils/math/Math.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketDAOProtocolSettingsMinipoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {RocketDAOProtocolSettingsNodeInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import {RocketNetworkPricesInterface} from "../../interface/network/RocketNetworkPricesInterface.sol"; import {RocketMinipoolManagerInterface} from "../../interface/minipool/RocketMinipoolManagerInterface.sol"; import {AddressSetStorageInterface} from "../../interface/util/AddressSetStorageInterface.sol"; import {RocketNetworkVotingInterface} from "../../interface/network/RocketNetworkVotingInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; /// @notice Accounting for snapshotting of governance related values based on block numbers contract RocketNetworkVoting is RocketBase, RocketNetworkVotingInterface { // Constants bytes32 immutable internal priceKey = keccak256("network.prices.rpl"); // Events event DelegateSet(address nodeOperator, address delegate, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 3; } /// @notice Returns the number of registered nodes at a given block /// @param _block Block number to query function getNodeCount(uint32 _block) external override view returns (uint256) { // Get contracts RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("node.count")); return uint256(rocketNetworkSnapshots.lookupRecent(key, _block, 10)); } /// @notice Returns the voting power of a given node operator at a specified block /// @param _nodeAddress Address of the node operator /// @param _block Block number to query function getVotingPower(address _nodeAddress, uint32 _block) external override view returns (uint256) { // Validate block number require(_block <= block.number, "Block must be in the past"); // Get contracts RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); // Setup bytes32 key; // Get ETH borrowed (minipools) key = keccak256(abi.encodePacked("eth.matched.node.amount", _nodeAddress)); uint256 borrowedETH = uint256(rocketNetworkSnapshots.lookupRecent(key, _block, 5)); // Get active minipools to calculate bonded ETH key = keccak256(abi.encodePacked("minipools.active.count", _nodeAddress)); uint256 activeMinipools = rocketNetworkSnapshots.lookupRecent(key, _block, 5); // Get total megapool bonded ETH key = keccak256(abi.encodePacked("megapool.eth.provided.node.amount", _nodeAddress)); uint256 megapoolETHBonded = rocketNetworkSnapshots.lookupRecent(key, _block, 5); // Calculate total bonded ETH uint256 totalETHStaked = (activeMinipools * 32 ether); uint256 bondedETH = totalETHStaked - borrowedETH + megapoolETHBonded; // Get RPL price uint256 rplPrice = uint256(rocketNetworkSnapshots.lookupRecent(priceKey, _block, 14)); // Get RPL staked by node operator key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); uint256 rplStake = uint256(rocketNetworkSnapshots.lookupRecent(key, _block, 5)); // Get RPL max stake percent key = keccak256(bytes("node.voting.power.stake.maximum")); uint256 maximumStakePercent = uint256(rocketNetworkSnapshots.lookupRecent(key, _block, 2)); return _calculateVotingPower(rplStake, bondedETH, rplPrice, maximumStakePercent); } /// @dev Calculates and returns a node's voting power based on the given inputs /// @param _rplStake Total RPL staked by a node (megapool + legacy staked RPL) /// @param _bondedETH Sum total of a node's bonded ETH /// @param _rplPrice The price of RPL in ETH /// @param _maxStakePercent The maximum RPL percentage that counts towards voting power function _calculateVotingPower(uint256 _rplStake, uint256 _bondedETH, uint256 _rplPrice, uint256 _maxStakePercent) internal pure returns (uint256) { uint256 maximumStake = _bondedETH * _maxStakePercent / _rplPrice; if (_rplStake > maximumStake) { _rplStake = maximumStake; } // Return the calculated voting power as the square root of clamped RPL stake return Math.sqrt(_rplStake * calcBase); } /// @notice Called by a registered node to set their delegate address /// @param _newDelegate The address of the node operator to delegate voting power to function setDelegate(address _newDelegate) external override onlyRegisteredNode(msg.sender) onlyRegisteredNode(_newDelegate) { // Get contracts RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); // Prevent setting to same value bytes32 key = keccak256(abi.encodePacked("node.delegate", msg.sender)); address existingDelegate = address(uint160(rocketNetworkSnapshots.latestValue(key))); require(_newDelegate != existingDelegate, "Delegate already set to value"); // Record new delegate rocketNetworkSnapshots.push(key, uint224(uint160(_newDelegate))); // Emit event emit DelegateSet(msg.sender, _newDelegate, block.timestamp); } /// @notice Returns the address of the node operator that the given node operator has delegated to at a given block /// @param _nodeAddress Address of the node operator to query /// @param _block The block number to query function getDelegate(address _nodeAddress, uint32 _block) external override view returns (address) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("node.delegate", _nodeAddress)); return address(uint160(rocketNetworkSnapshots.lookupRecent(key, _block, 10))); } /// @notice Returns the address of the node operator that the given node operator is currently delegate to /// @param _nodeAddress Address of the node operator to query function getCurrentDelegate(address _nodeAddress) external override view returns (address) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("node.delegate", _nodeAddress)); return address(uint160(rocketNetworkSnapshots.latestValue(key))); } } ================================================ FILE: contracts/contract/node/RocketNodeDeposit.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; import {RocketDAOProtocolSettingsNodeInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import {RocketDepositPoolInterface} from "../../interface/deposit/RocketDepositPoolInterface.sol"; import {RocketMegapoolFactoryInterface} from "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; import {RocketMegapoolInterface} from "../../interface/megapool/RocketMegapoolInterface.sol"; import {RocketMegapoolManagerInterface} from "../../interface/megapool/RocketMegapoolManagerInterface.sol"; import {RocketNodeDepositInterface} from "../../interface/node/RocketNodeDepositInterface.sol"; import {RocketBase} from "../RocketBase.sol"; /// @notice Entry point for node operators to perform deposits for the creation of new validators on the network contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface, RocketVaultWithdrawerInterface { // Constants uint256 constant internal pubKeyLength = 48; uint256 constant internal signatureLength = 96; // Events event DepositReceived(address indexed from, uint256 amount, uint256 time); event MultiDepositReceived(address indexed from, uint256 numberOfValidators, uint256 totalBond, uint256 time); event DepositFor(address indexed nodeAddress, address indexed from, uint256 amount, uint256 time); event Withdrawal(address indexed nodeAddress, address indexed to, uint256 amount, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 5; } /// @notice Accept incoming ETH from the deposit pool receive() external payable onlyLatestContract("rocketDepositPool", msg.sender) {} /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyLatestContract("rocketVault", msg.sender) {} /// @notice Returns the bond requirement for the given number of validators /// @param _numValidators The number of validator to calculate the bond requirement for function getBondRequirement(uint256 _numValidators) override public view returns (uint256) { if (_numValidators == 0) { return 0; } // Get contracts RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); // Calculate bond requirement (per RPIP-42) uint256[] memory baseBondArray = rocketDAOProtocolSettingsNode.getBaseBondArray(); if (_numValidators - 1 < baseBondArray.length) { return baseBondArray[_numValidators - 1]; } uint256 reducedBond = rocketDAOProtocolSettingsNode.getReducedBond(); return baseBondArray[baseBondArray.length - 1] + (_numValidators - baseBondArray.length) * reducedBond; } /// @notice Returns a node operator's credit balance in ETH /// @param _nodeAddress Address of the node operator to query for function getNodeDepositCredit(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress))); } /// @notice Returns the current ETH balance for the given node operator /// @param _nodeAddress Address of the node operator to query for function getNodeEthBalance(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.eth.balance", _nodeAddress))); } /// @notice Returns the sum of the credit balance of a given node operator and their balance /// @param _nodeAddress Address of the node operator to query for function getNodeCreditAndBalance(address _nodeAddress) override external view returns (uint256) { return getNodeDepositCredit(_nodeAddress) + getNodeEthBalance(_nodeAddress); } /// @notice Returns the sum of the amount of ETH credit currently usable by a given node operator and their balance /// @param _nodeAddress Address of the node operator to query for function getNodeUsableCreditAndBalance(address _nodeAddress) override external view returns (uint256) { return getNodeUsableCredit(_nodeAddress) + getNodeEthBalance(_nodeAddress); } /// @notice Returns the amount of ETH credit currently usable by a given node operator /// @param _nodeAddress Address of the node operator to query for function getNodeUsableCredit(address _nodeAddress) override public view returns (uint256) { RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); uint256 depositPoolBalance = rocketDepositPool.getBalance(); uint256 usableCredit = getNodeDepositCredit(_nodeAddress); if (usableCredit > depositPoolBalance) { usableCredit = depositPoolBalance; } return usableCredit; } /// @dev Increases a node operators deposit credit balance /// @param _nodeAddress Address of the node operator to increase deposit balance for /// @param _amount Amount to increase deposit credit balance by function increaseDepositCreditBalance(address _nodeAddress, uint256 _amount) override external onlyLatestContract("rocketNodeDeposit", address(this)) { // Accept calls from registered minipools require(getBool(keccak256(abi.encodePacked("minipool.exists", msg.sender))), "Invalid or outdated network contract"); // Increase credit balance addUint(keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress)), _amount); } /// @notice Deposits ETH for the given node operator /// @param _nodeAddress The address of the node operator to deposit ETH for function depositEthFor(address _nodeAddress) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(_nodeAddress) { // Send the ETH to vault uint256 amount = msg.value; RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.depositEther{value: amount}(); // Increment balance addUint(keccak256(abi.encodePacked("node.eth.balance", _nodeAddress)), amount); // Log it emit DepositFor(_nodeAddress, msg.sender, amount, block.timestamp); } /// @notice Withdraws ETH from a node operator's balance. Must be called from withdrawal address. /// @param _nodeAddress Address of the node operator to withdraw from /// @param _amount Amount of ETH to withdraw function withdrawEth(address _nodeAddress, uint256 _amount) external onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(_nodeAddress) { // Check valid caller address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); require(msg.sender == withdrawalAddress, "Only withdrawal address can withdraw ETH"); // Check balance and update uint256 balance = getNodeEthBalance(_nodeAddress); require(balance >= _amount, "Insufficient balance"); setUint(keccak256(abi.encodePacked("node.eth.balance", _nodeAddress)), balance - _amount); // Withdraw the funds RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.withdrawEther(_amount); // Send funds to withdrawalAddress (bool success,) = withdrawalAddress.call{value: _amount}(""); require(success, "Failed to withdraw ETH"); // Log it emit Withdrawal(_nodeAddress, withdrawalAddress, _amount, block.timestamp); } /// @notice Accept a node deposit and create a new validator under the node. Only accepts calls from registered nodes /// @param _bondAmount The amount of capital the node operator wants to put up as his bond /// @param _useExpressTicket If the express queue should be used /// @param _validatorPubkey Pubkey of the validator the node operator wishes to migrate /// @param _validatorSignature Signature from the validator over the deposit data /// @param _depositDataRoot The hash tree root of the deposit data (passed onto the deposit contract on pre stake) function deposit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(msg.sender) { // Check amount require(msg.value == _bondAmount, "Invalid value"); // Process the deposit _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot, msg.value); } /// @notice Accept a node deposit and create a new validator under the node. Only accepts calls from registered nodes /// @param _bondAmount The amount of capital the node operator wants to put up as his bond /// @param _useExpressTicket If the express queue should be used /// @param _validatorPubkey Pubkey of the validator the node operator wishes to migrate /// @param _validatorSignature Signature from the validator over the deposit data /// @param _depositDataRoot The hash tree root of the deposit data (passed onto the deposit contract on pre stake) function depositWithCredit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(msg.sender) { // Process the deposit uint256 balanceToUse = _useCreditOrBalanceIfRequired(_bondAmount); _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot, msg.value + balanceToUse); } /// @notice Processes multiple node deposits in one call /// @param _deposits Array of deposits to process function depositMulti(NodeDeposit[] calldata _deposits) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(msg.sender) { // Check pre-conditions require(_deposits.length > 0, "Must perform at least 1 deposit"); _checkDepositsEnabled(); // Get or deploy a megapool for the caller RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); RocketMegapoolInterface megapool = RocketMegapoolInterface(rocketMegapoolFactory.getOrDeployContract(msg.sender)); RocketMegapoolManagerInterface rocketMegapoolManager = RocketMegapoolManagerInterface(getContractAddress("rocketMegapoolManager")); // Iterate deposits and execute uint256 totalBond = 0; for (uint256 i = 0; i < _deposits.length; ++i) { NodeDeposit calldata deposit = _deposits[i]; // Validate arguments _validateBytes(deposit.validatorPubkey, pubKeyLength); _validateBytes(deposit.validatorSignature, signatureLength); // Request a new validator from the megapool rocketMegapoolManager.addValidator(address(megapool), megapool.getValidatorCount(), deposit.validatorPubkey); megapool.newValidator(deposit.bondAmount, deposit.useExpressTicket, deposit.validatorPubkey, deposit.validatorSignature, deposit.depositDataRoot); // Sum bond total totalBond += deposit.bondAmount; } // Check if node accidentally sent too high msg.value require(msg.value <= totalBond, "Excess bond value supplied"); // Check if node sent full bond amount of if we need to use credit/balance uint256 balanceToUse = 0; if (msg.value < totalBond) { balanceToUse = _useCreditOrBalanceIfRequired(totalBond); } // Emit deposit received event emit MultiDepositReceived(msg.sender, _deposits.length, totalBond, block.timestamp); // Send node operator's bond to the deposit pool RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); rocketDepositPool.nodeDeposit{value: msg.value + balanceToUse}(totalBond); // Attempt to assign 1 minipool/megapool for each deposit rocketDepositPool.maybeAssignDeposits(_deposits.length); } /// @dev Internal logic to process a deposit /// @param _bondAmount The amount of capital the node operator wants to put up as his bond /// @param _useExpressTicket If the express queue should be used /// @param _validatorPubkey Pubkey of the validator the node operator wishes to migrate /// @param _validatorSignature Signature from the validator over the deposit data /// @param _depositDataRoot The hash tree root of the deposit data (passed onto the deposit contract on pre stake) /// @param _value Total value of the deposit including any credit balance used function _deposit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot, uint256 _value) internal { // Validate arguments _validateBytes(_validatorPubkey, pubKeyLength); _validateBytes(_validatorSignature, signatureLength); // Check pre-conditions _checkDepositsEnabled(); // Emit deposit received event emit DepositReceived(msg.sender, _value, block.timestamp); // Get or deploy a megapool for the caller RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); RocketMegapoolInterface megapool = RocketMegapoolInterface(rocketMegapoolFactory.getOrDeployContract(msg.sender)); RocketMegapoolManagerInterface rocketMegapoolManager = RocketMegapoolManagerInterface(getContractAddress("rocketMegapoolManager")); // Request a new validator from the megapool rocketMegapoolManager.addValidator(address(megapool), megapool.getValidatorCount(), _validatorPubkey); megapool.newValidator(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot); // Send node operator's bond to the deposit pool RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); rocketDepositPool.nodeDeposit{value: _value}(_bondAmount); // Attempt to assign 1 minipool/megapool rocketDepositPool.maybeAssignDeposits(1); } /// @dev Reverts if deposits are not enabled function _checkDepositsEnabled() internal { // Get contracts RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); // Check node settings require(rocketDAOProtocolSettingsNode.getDepositEnabled(), "Node deposits are currently disabled"); } /// @notice Validates that a byte array has the expected length /// @param _data the byte array being validated /// @param _length the expected length function _validateBytes(bytes memory _data, uint256 _length) pure internal { require(_data.length == _length, "Invalid bytes length"); } /// @dev If msg.value does not cover the bond amount, take from node's credit / balance to make up the difference /// Reverts if node does not have enough credit or ETH balance to cover the shortfall /// @return Returns the amount of ETH withdrawn from the vault from the node's ETH balance function _useCreditOrBalanceIfRequired(uint256 _bondAmount) internal returns (uint256) { uint256 balanceToUse = 0; uint256 creditToUse = 0; uint256 shortFall = _bondAmount - msg.value; uint256 credit = getNodeUsableCredit(msg.sender); uint256 balance = getNodeEthBalance(msg.sender); // Check credit require(credit + balance >= shortFall, "Insufficient credit"); // Calculate amounts to use creditToUse = shortFall; if (credit < shortFall) { balanceToUse = shortFall - credit; creditToUse = credit; } // Update balances if (balanceToUse > 0) { subUint(keccak256(abi.encodePacked("node.eth.balance", msg.sender)), balanceToUse); // Withdraw the funds RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.withdrawEther(balanceToUse); } if (creditToUse > 0) { subUint(keccak256(abi.encodePacked("node.deposit.credit.balance", msg.sender)), creditToUse); } return balanceToUse; } } ================================================ FILE: contracts/contract/node/RocketNodeDistributor.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "./RocketNodeDistributorStorageLayout.sol"; contract RocketNodeDistributor is RocketNodeDistributorStorageLayout { bytes32 immutable distributorStorageKey; constructor(address _nodeAddress, address _rocketStorage) { rocketStorage = RocketStorageInterface(_rocketStorage); nodeAddress = _nodeAddress; // Precompute storage key for rocketNodeDistributorDelegate distributorStorageKey = keccak256(abi.encodePacked("contract.address", "rocketNodeDistributorDelegate")); } // Allow contract to receive ETH without making a delegated call receive() external payable {} // Delegates all transactions to the target supplied during creation fallback() external payable { address _target = rocketStorage.getAddress(distributorStorageKey); assembly { calldatacopy(0x0, 0x0, calldatasize()) let result := delegatecall(gas(), _target, 0x0, calldatasize(), 0x0, 0) returndatacopy(0x0, 0x0, returndatasize()) switch result case 0 {revert(0, returndatasize())} default {return (0, returndatasize())} } } } ================================================ FILE: contracts/contract/node/RocketNodeDistributorDelegate.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketNodeDistributorInterface} from "../../interface/node/RocketNodeDistributorInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {RocketNodeDistributorStorageLayout} from "./RocketNodeDistributorStorageLayout.sol"; /// @dev Contains the logic for RocketNodeDistributors contract RocketNodeDistributorDelegate is RocketNodeDistributorStorageLayout, RocketNodeDistributorInterface { // Events event FeesDistributed(address _nodeAddress, uint256 _userAmount, uint256 _nodeAmount, uint256 _time); // Constants uint8 public constant version = 3; uint256 internal constant calcBase = 1 ether; uint256 internal constant NOT_ENTERED = 1; uint256 internal constant ENTERED = 2; // Precomputed constants bytes32 internal constant rocketNodeManagerKey = keccak256(abi.encodePacked("contract.address", "rocketNodeManager")); bytes32 internal constant rocketNodeStakingKey = keccak256(abi.encodePacked("contract.address", "rocketNodeStaking")); bytes32 internal constant rocketTokenRETHKey = keccak256(abi.encodePacked("contract.address", "rocketTokenRETH")); modifier nonReentrant() { require(lock != ENTERED, "Reentrant call"); lock = ENTERED; _; lock = NOT_ENTERED; } constructor() { // These values must be set by proxy contract as this contract should only be delegatecalled rocketStorage = RocketStorageInterface(address(0)); nodeAddress = address(0); lock = NOT_ENTERED; } /// @notice Returns the portion of the contract's balance that belongs to the node operator function getNodeShare() override public view returns (uint256) { // Get contracts RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(rocketStorage.getAddress(rocketNodeManagerKey)); RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(rocketStorage.getAddress(rocketNodeStakingKey)); // Get withdrawal address and the node's average node fee uint256 averageNodeFee = rocketNodeManager.getAverageNodeFee(nodeAddress); // Get node ETH collateral ratio uint256 collateralRatio = rocketNodeStaking.getNodeETHCollateralisationRatio(nodeAddress); // Calculate reward split uint256 nodeBalance = address(this).balance * calcBase / collateralRatio; uint256 userBalance = address(this).balance - nodeBalance; return nodeBalance + (userBalance * averageNodeFee / calcBase); } /// @notice Returns the portion of the contract's balance that belongs to the users function getUserShare() override external view returns (uint256) { return address(this).balance - getNodeShare(); } /// @notice Distributes the balance of this contract to its owners function distribute() override external nonReentrant { // Calculate node share uint256 nodeShare = getNodeShare(); // Transfer node share address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); if (msg.sender == nodeAddress || msg.sender == withdrawalAddress) { // If called by node operator, transfer directly (bool success,) = withdrawalAddress.call{value: nodeShare}(""); require(success, "Failed to send funds to withdrawal address"); } else { // If not called by node operator, add to unclaimed balance for later claiming RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(rocketStorage.getAddress(rocketNodeManagerKey)); rocketNodeManager.addUnclaimedRewards{value: nodeShare}(nodeAddress); } // Transfer user share uint256 userShare = address(this).balance; address rocketTokenRETH = rocketStorage.getAddress(rocketTokenRETHKey); payable(rocketTokenRETH).transfer(userShare); // Emit event emit FeesDistributed(nodeAddress, userShare, nodeShare, block.timestamp); } } ================================================ FILE: contracts/contract/node/RocketNodeDistributorFactory.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "./RocketNodeDistributor.sol"; import "./RocketNodeDistributorStorageLayout.sol"; import "../../interface/node/RocketNodeDistributorFactoryInterface.sol"; contract RocketNodeDistributorFactory is RocketBase, RocketNodeDistributorFactoryInterface { // Events event ProxyCreated(address _address); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } function getProxyBytecode() override public pure returns (bytes memory) { return type(RocketNodeDistributor).creationCode; } // Calculates the predetermined distributor contract address from given node address function getProxyAddress(address _nodeAddress) override external view returns(address) { bytes memory contractCode = getProxyBytecode(); bytes memory initCode = abi.encodePacked(contractCode, abi.encode(_nodeAddress, rocketStorage)); bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), uint256(0), keccak256(initCode))); return address(uint160(uint(hash))); } // Uses CREATE2 to deploy a RocketNodeDistributor at predetermined address function createProxy(address _nodeAddress) override external onlyLatestContract("rocketNodeManager", msg.sender) { // Salt is not required as the initCode is already unique per node address (node address is constructor argument) RocketNodeDistributor dist = new RocketNodeDistributor{salt: ''}(_nodeAddress, address(rocketStorage)); emit ProxyCreated(address(dist)); } } ================================================ FILE: contracts/contract/node/RocketNodeDistributorStorageLayout.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; abstract contract RocketNodeDistributorStorageLayout { RocketStorageInterface internal rocketStorage; address internal nodeAddress; uint256 internal lock; // Reentrancy guard } ================================================ FILE: contracts/contract/node/RocketNodeManager.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; import {RocketDAONodeTrustedSettingsRewardsInterface} from "../../interface/dao/node/settings/RocketDAONodeTrustedSettingsRewardsInterface.sol"; import {RocketDAOProtocolSettingsDepositInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsDepositInterface.sol"; import {RocketDAOProtocolSettingsMinipoolInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import {RocketDAOProtocolSettingsNodeInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import {RocketDAOProtocolSettingsRewardsInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import {RocketMegapoolFactoryInterface} from "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; import {RocketMinipoolInterface} from "../../interface/minipool/RocketMinipoolInterface.sol"; import {RocketMinipoolManagerInterface} from "../../interface/minipool/RocketMinipoolManagerInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketNodeDistributorFactoryInterface} from "../../interface/node/RocketNodeDistributorFactoryInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {AddressSetStorageInterface} from "../../interface/util/AddressSetStorageInterface.sol"; import {MinipoolStatus} from "../../types/MinipoolStatus.sol"; import {RocketBase} from "../RocketBase.sol"; /// @notice Node registration and management contract RocketNodeManager is RocketBase, RocketNodeManagerInterface, RocketVaultWithdrawerInterface { // Events event NodeRegistered(address indexed node, uint256 time); event NodeTimezoneLocationSet(address indexed node, uint256 time); event NodeRewardNetworkChanged(address indexed node, uint256 network); event NodeSmoothingPoolStateChanged(address indexed node, bool state); event NodeRPLWithdrawalAddressSet(address indexed node, address indexed withdrawalAddress, uint256 time); event NodeRPLWithdrawalAddressUnset(address indexed node, uint256 time); event NodeUnclaimedRewardsAdded(address indexed node, uint256 amount, uint256 time); event NodeUnclaimedRewardsClaimed(address indexed node, uint256 amount, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 5; } /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyLatestContract("rocketNodeManager", address(this)) onlyLatestContract("rocketVault", msg.sender) {} /// @notice Get the number of nodes in the network function getNodeCount() override public view returns (uint256) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getCount(keccak256(abi.encodePacked("nodes.index"))); } /// @notice Get a breakdown of the number of nodes per timezone /// @dev Iterating the entire set may exceed gas limit so caller can paginate using _offset and _limit function getNodeCountPerTimezone(uint256 _offset, uint256 _limit) override external view returns (TimezoneCount[] memory) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Precompute node key bytes32 nodeKey = keccak256(abi.encodePacked("nodes.index")); // Calculate range uint256 totalNodes = addressSetStorage.getCount(nodeKey); uint256 max = _offset + _limit; if (max > totalNodes || _limit == 0) {max = totalNodes;} // Create an array with as many elements as there are potential values to return TimezoneCount[] memory counts = new TimezoneCount[](max - _offset); uint256 uniqueTimezoneCount = 0; // Iterate the minipool range for (uint256 i = _offset; i < max; ++i) { address nodeAddress = addressSetStorage.getItem(nodeKey, i); string memory timezone = getString(keccak256(abi.encodePacked("node.timezone.location", nodeAddress))); // Find existing entry in our array bool existing = false; for (uint256 j = 0; j < uniqueTimezoneCount; ++j) { if (keccak256(bytes(counts[j].timezone)) == keccak256(bytes(timezone))) { existing = true; // Increment the counter counts[j].count++; break; } } // Entry was not found, so create a new one if (!existing) { counts[uniqueTimezoneCount].timezone = timezone; counts[uniqueTimezoneCount].count = 1; uniqueTimezoneCount++; } } // Dirty hack to cut unused elements off end of return value assembly { mstore(counts, uniqueTimezoneCount) } return counts; } /// @notice Get a node address by index function getNodeAt(uint256 _index) override external view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); return addressSetStorage.getItem(keccak256(abi.encodePacked("nodes.index")), _index); } /// @notice Check whether a node exists function getNodeExists(address _nodeAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("node.exists", _nodeAddress))); } /// @notice Get a node's current withdrawal address function getNodeWithdrawalAddress(address _nodeAddress) override public view returns (address) { return rocketStorage.getNodeWithdrawalAddress(_nodeAddress); } /// @notice Get a node's pending withdrawal address function getNodePendingWithdrawalAddress(address _nodeAddress) override public view returns (address) { return rocketStorage.getNodePendingWithdrawalAddress(_nodeAddress); } /// @notice Get a node's current RPL withdrawal address function getNodeRPLWithdrawalAddress(address _nodeAddress) override public view returns (address) { address withdrawalAddress = getAddress(keccak256(abi.encodePacked("node.rpl.withdrawal.address", _nodeAddress))); if (withdrawalAddress == address(0)) { // Defaults to current withdrawal address if unset return rocketStorage.getNodeWithdrawalAddress(_nodeAddress); } return withdrawalAddress; } /// @notice Get a node's pending RPL withdrawal address /// @param _nodeAddress Address of the node to query function getNodePendingRPLWithdrawalAddress(address _nodeAddress) override public view returns (address) { return getAddress(keccak256(abi.encodePacked("node.pending.rpl.withdrawal.address", _nodeAddress))); } /// @notice Returns true if a node has set an RPL withdrawal address /// @param _nodeAddress Address of the node to query function getNodeRPLWithdrawalAddressIsSet(address _nodeAddress) override external view returns (bool) { return (getAddress(keccak256(abi.encodePacked("node.rpl.withdrawal.address", _nodeAddress))) != address(0)); } /// @notice Unsets a node operator's RPL withdrawal address returning it to the default /// @param _nodeAddress Address of the node to query function unsetRPLWithdrawalAddress(address _nodeAddress) external override onlyRegisteredNode(_nodeAddress) { bytes32 addressKey = keccak256(abi.encodePacked("node.rpl.withdrawal.address", _nodeAddress)); // Confirm the transaction is from the node's current RPL withdrawal address require(getAddress(addressKey) == msg.sender, "Only a tx from a node's RPL withdrawal address can unset it"); // Unset the address deleteAddress(addressKey); // Emit withdrawal address unset event emit NodeRPLWithdrawalAddressUnset(_nodeAddress, block.timestamp); } // @notice Set a node's RPL withdrawal address /// @param _nodeAddress Address of the node to set RPL withdrawal address for /// @param _newRPLWithdrawalAddress The new RPL withdrawal address to set /// @param _confirm Whether to instantly make the change or requires a confirmation from the new address function setRPLWithdrawalAddress(address _nodeAddress, address _newRPLWithdrawalAddress, bool _confirm) external override onlyRegisteredNode(_nodeAddress) { // Check new RPL withdrawal address require(_newRPLWithdrawalAddress != address(0x0), "Invalid RPL withdrawal address"); // Confirm the transaction is from the node's current RPL withdrawal address address withdrawalAddress = getNodeRPLWithdrawalAddress(_nodeAddress); require(withdrawalAddress == msg.sender, "Only a tx from a node's RPL withdrawal address can update it"); // Update immediately if confirmed if (_confirm) { // Delete any existing pending update deleteAddress(keccak256(abi.encodePacked("node.pending.rpl.withdrawal.address", _nodeAddress))); // Perform the update updateRPLWithdrawalAddress(_nodeAddress, _newRPLWithdrawalAddress); } else { // Set pending withdrawal address immediately setAddress(keccak256(abi.encodePacked("node.pending.rpl.withdrawal.address", _nodeAddress)), _newRPLWithdrawalAddress); } } /// @notice Confirm a node's new RPL withdrawal address /// @param _nodeAddress Address of the node to confirm new RPL withdrawal address for function confirmRPLWithdrawalAddress(address _nodeAddress) external override onlyRegisteredNode(_nodeAddress) { bytes32 pendingKey = keccak256(abi.encodePacked("node.pending.rpl.withdrawal.address", _nodeAddress)); // Get node by pending withdrawal address require(getAddress(pendingKey) == msg.sender, "Confirmation must come from the pending RPL withdrawal address"); deleteAddress(pendingKey); // Update withdrawal address updateRPLWithdrawalAddress(_nodeAddress, msg.sender); } /// @dev Internal implementation of updating a node's RPL withdrawal address /// @param _nodeAddress Address of the node to set RPL withdrawal address for /// @param _newRPLWithdrawalAddress The new RPL withdrawal address to set function updateRPLWithdrawalAddress(address _nodeAddress, address _newRPLWithdrawalAddress) private { // Set new withdrawal address setAddress(keccak256(abi.encodePacked("node.rpl.withdrawal.address", _nodeAddress)), _newRPLWithdrawalAddress); // Emit withdrawal address set event emit NodeRPLWithdrawalAddressSet(_nodeAddress, _newRPLWithdrawalAddress, block.timestamp); } /// @notice Get a node's timezone location /// @param _nodeAddress Address of the node to query function getNodeTimezoneLocation(address _nodeAddress) override public view returns (string memory) { return getString(keccak256(abi.encodePacked("node.timezone.location", _nodeAddress))); } /// @notice Register a new node with Rocket Pool /// @param _timezoneLocation Timezone of the node operator (used only as a hint to the protocol about its geographic diversity) function registerNode(string calldata _timezoneLocation) override external onlyLatestContract("rocketNodeManager", address(this)) { // Load contracts RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); // Check node settings require(rocketDAOProtocolSettingsNode.getRegistrationEnabled(), "Rocket Pool node registrations are currently disabled"); // Check timezone location require(bytes(_timezoneLocation).length >= 4, "The timezone location is invalid"); // Initialise node data setBool(keccak256(abi.encodePacked("node.exists", msg.sender)), true); setBool(keccak256(abi.encodePacked("node.voting.enabled", msg.sender)), true); setString(keccak256(abi.encodePacked("node.timezone.location", msg.sender)), _timezoneLocation); setBool(keccak256(abi.encodePacked("node.express.provisioned", msg.sender)), true); setUint(keccak256(abi.encodePacked("node.express.tickets", msg.sender)), rocketDAOProtocolSettingsDeposit.getExpressQueueTicketsBaseProvision()); // Add node to index bytes32 nodeIndexKey = keccak256(abi.encodePacked("nodes.index")); addressSetStorage.addItem(nodeIndexKey, msg.sender); // Set node registration time (uses old storage key name for backwards compatibility) setUint(keccak256(abi.encodePacked("rewards.pool.claim.contract.registered.time", "rocketClaimNode", msg.sender)), block.timestamp); // Update count rocketNetworkSnapshots.push(keccak256(abi.encodePacked("node.count")), uint224(addressSetStorage.getCount(nodeIndexKey))); // Default voting delegate to themself rocketNetworkSnapshots.push(keccak256(abi.encodePacked("node.delegate", msg.sender)), uint224(uint160(msg.sender))); // Emit node registered event emit NodeRegistered(msg.sender, block.timestamp); } /// @notice Gets the timestamp of when a node was registered /// @param _nodeAddress Address of the node to query function getNodeRegistrationTime(address _nodeAddress) onlyRegisteredNode(_nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("rewards.pool.claim.contract.registered.time", "rocketClaimNode", _nodeAddress))); } /// @notice Set a node's timezone location /// @param _timezoneLocation New timezone of the node operator function setTimezoneLocation(string calldata _timezoneLocation) override external onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(msg.sender) { // Check timezone location require(bytes(_timezoneLocation).length >= 4, "The timezone location is invalid"); // Set timezone location setString(keccak256(abi.encodePacked("node.timezone.location", msg.sender)), _timezoneLocation); // Emit node timezone location set event emit NodeTimezoneLocationSet(msg.sender, block.timestamp); } /// @notice Returns true if node has initialised their fee distributor contract /// @param _nodeAddress Address of the node to query function getFeeDistributorInitialised(address _nodeAddress) override public view returns (bool) { // Load contracts RocketNodeDistributorFactoryInterface rocketNodeDistributorFactory = RocketNodeDistributorFactoryInterface(getContractAddress("rocketNodeDistributorFactory")); // Get distributor address address contractAddress = rocketNodeDistributorFactory.getProxyAddress(_nodeAddress); // Check if contract exists at that address uint32 codeSize; assembly { codeSize := extcodesize(contractAddress) } return codeSize > 0; } /// @notice Node operators created before the distributor was implemented must call this to setup their distributor contract /// @dev Fee distributor is no longer used but this function is provided for backwards compatibility for existing node operators who never initialised theirs function initialiseFeeDistributor() override external onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(msg.sender) { // Prevent multiple calls require(!getFeeDistributorInitialised(msg.sender), "Already initialised"); // Load contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Calculate and set current average fee numerator uint256 count = rocketMinipoolManager.getNodeMinipoolCount(msg.sender); if (count > 0) { uint256 numerator = 0; // Note: this loop is safe as long as all current node operators at the time of upgrade have few enough minipools for (uint256 i = 0; i < count; ++i) { RocketMinipoolInterface minipool = RocketMinipoolInterface(rocketMinipoolManager.getNodeMinipoolAt(msg.sender, i)); if (minipool.getStatus() == MinipoolStatus.Staking) { numerator = numerator + minipool.getNodeFee(); } } setUint(keccak256(abi.encodePacked("node.average.fee.numerator", msg.sender)), numerator); } // Create the distributor contract _initialiseFeeDistributor(msg.sender); } /// @dev Deploys the fee distributor contract for a given node function _initialiseFeeDistributor(address _nodeAddress) internal { // Load contracts RocketNodeDistributorFactoryInterface rocketNodeDistributorFactory = RocketNodeDistributorFactoryInterface(getContractAddress("rocketNodeDistributorFactory")); // Create the distributor proxy rocketNodeDistributorFactory.createProxy(_nodeAddress); } /// @notice Calculates a node's average node fee (for minipools) /// @param _nodeAddress Address of the node to query function getAverageNodeFee(address _nodeAddress) override external view returns (uint256) { // Load contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Get valid deposit amounts uint256[2] memory depositSizes; depositSizes[0] = 16 ether; depositSizes[1] = 8 ether; // Setup memory for calculations uint256[] memory depositWeights = new uint256[](depositSizes.length); uint256[] memory depositCounts = new uint256[](depositSizes.length); uint256 depositWeightTotal; uint256 totalCount; uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); // Retrieve the number of staking minipools per deposit size for (uint256 i = 0; i < depositSizes.length; ++i) { depositCounts[i] = rocketMinipoolManager.getNodeStakingMinipoolCountBySize(_nodeAddress, depositSizes[i]); totalCount = totalCount + depositCounts[i]; } if (totalCount == 0) { return 0; } // Calculate the weights of each deposit size for (uint256 i = 0; i < depositSizes.length; ++i) { depositWeights[i] = (launchAmount - depositSizes[i]) * depositCounts[i]; depositWeightTotal = depositWeightTotal + depositWeights[i]; } for (uint256 i = 0; i < depositSizes.length; ++i) { depositWeights[i] = depositWeights[i] * calcBase / depositWeightTotal; } // Calculate the weighted average uint256 weightedAverage = 0; for (uint256 i = 0; i < depositSizes.length; ++i) { if (depositCounts[i] > 0) { bytes32 numeratorKey; if (depositSizes[i] == 16 ether) { numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress)); } else { numeratorKey = keccak256(abi.encodePacked("node.average.fee.numerator", _nodeAddress, depositSizes[i])); } uint256 numerator = getUint(numeratorKey); weightedAverage = weightedAverage + (numerator * depositWeights[i] / depositCounts[i]); } } return weightedAverage / calcBase; } /// @notice Designates which network a node would like their rewards relayed to /// @param _nodeAddress Address of the node to set reward network for /// @param _network ID of the network function setRewardNetwork(address _nodeAddress, uint256 _network) override external onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(_nodeAddress) { // Confirm the transaction is from the node's current withdrawal address address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); require(withdrawalAddress == msg.sender, "Only a tx from a node's withdrawal address can change reward network"); // Check network is enabled RocketDAONodeTrustedSettingsRewardsInterface rocketDAONodeTrustedSettingsRewards = RocketDAONodeTrustedSettingsRewardsInterface(getContractAddress("rocketDAONodeTrustedSettingsRewards")); require(rocketDAONodeTrustedSettingsRewards.getNetworkEnabled(_network), "Network is not enabled"); // Set the network setUint(keccak256(abi.encodePacked("node.reward.network", _nodeAddress)), _network); // Emit event emit NodeRewardNetworkChanged(_nodeAddress, _network); } /// @notice Returns which network a node has designated as their desired reward network /// @param _nodeAddress Address of the node to query function getRewardNetwork(address _nodeAddress) override public view onlyLatestContract("rocketNodeManager", address(this)) returns (uint256) { return getUint(keccak256(abi.encodePacked("node.reward.network", _nodeAddress))); } /// @notice Allows a node to register or deregister from the smoothing pool /// @param _state True to opt in to the smoothing pool or false otherwise function setSmoothingPoolRegistrationState(bool _state) override external onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(msg.sender) { // Ensure registration is enabled RocketDAOProtocolSettingsNodeInterface daoSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); require(daoSettingsNode.getSmoothingPoolRegistrationEnabled(), "Smoothing pool registrations are not active"); // Precompute storage keys bytes32 changeKey = keccak256(abi.encodePacked("node.smoothing.pool.changed.time", msg.sender)); bytes32 stateKey = keccak256(abi.encodePacked("node.smoothing.pool.state", msg.sender)); // Get from the DAO settings RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); uint256 rewardInterval = daoSettingsRewards.getRewardsClaimIntervalTime(); // Ensure node operator has waited the required time uint256 lastChange = getUint(changeKey); require(block.timestamp >= lastChange + rewardInterval, "Not enough time has passed since changing state"); // Ensure state is actually changing require(getBool(stateKey) != _state, "Invalid state change"); // Update registration state setUint(changeKey, block.timestamp); setBool(stateKey, _state); // Emit state change event emit NodeSmoothingPoolStateChanged(msg.sender, _state); } /// @notice Returns whether a node is registered or not from the smoothing pool /// @param _nodeAddress Address of the node to query function getSmoothingPoolRegistrationState(address _nodeAddress) override public view returns (bool) { return getBool(keccak256(abi.encodePacked("node.smoothing.pool.state", _nodeAddress))); } /// @notice Returns the timestamp of when the node last changed their smoothing pool registration state /// @param _nodeAddress Address of the node to query function getSmoothingPoolRegistrationChanged(address _nodeAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.smoothing.pool.changed.time", _nodeAddress))); } /// @notice Returns the sum of nodes that are registered for the smoothing pool between _offset and (_offset + _limit) function getSmoothingPoolRegisteredNodeCount(uint256 _offset, uint256 _limit) override external view returns (uint256) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Precompute node key bytes32 nodeKey = keccak256(abi.encodePacked("nodes.index")); // Iterate over the requested minipool range uint256 totalNodes = getNodeCount(); uint256 max = _offset + _limit; if (max > totalNodes || _limit == 0) {max = totalNodes;} uint256 count = 0; for (uint256 i = _offset; i < max; ++i) { address nodeAddress = addressSetStorage.getItem(nodeKey, i); if (getSmoothingPoolRegistrationState(nodeAddress)) { count++; } } return count; } /// @notice Returns a slice of the node operator address set /// @param _offset The starting point for the slice /// @param _limit The maximum number of results to return in the slice function getNodeAddresses(uint256 _offset, uint256 _limit) override external view returns (address[] memory) { // Get contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage")); // Precompute node key bytes32 nodeKey = keccak256(abi.encodePacked("nodes.index")); // Iterate over the requested minipool range uint256 totalNodes = getNodeCount(); uint256 max = _offset + _limit; if (max > totalNodes || _limit == 0) {max = totalNodes;} // Create array big enough for every minipool address[] memory nodes = new address[](max - _offset); uint256 total = 0; for (uint256 i = _offset; i < max; ++i) { nodes[total] = addressSetStorage.getItem(nodeKey, i); total++; } // Dirty hack to cut unused elements off end of return value assembly { mstore(nodes, total) } return nodes; } /// @notice Deploys a single Megapool contract for the calling node operator function deployMegapool() override external onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(msg.sender) returns (address) { RocketMegapoolFactoryInterface rocketMegapool = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); require(!rocketMegapool.getMegapoolDeployed(msg.sender), "Megapool already deployed for this node"); return rocketMegapool.deployContract(msg.sender); } /// @notice Returns the number of express tickets the given node has /// @param _nodeAddress Address of the node operator to query function getExpressTicketCount(address _nodeAddress) public override view returns (uint256) { bool provisioned = getBool(keccak256(abi.encodePacked("node.express.provisioned", _nodeAddress))); uint256 expressTickets = 0; if (!provisioned) { // Nodes prior to Saturn should receive 0 express tickets (initial value of `express_queue_tickets_base_provision`) (RPIP-75) expressTickets += 0; // Each node SHALL be provided additional express_queue_tickets equal to (bonded ETH in legacy minipools)/4 RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); uint256 bondedETH = rocketNodeStaking.getNodeETHBonded(_nodeAddress); expressTickets += bondedETH / 4 ether; } expressTickets += getUint(keccak256(abi.encodePacked("node.express.tickets", _nodeAddress))); return expressTickets; } /// @notice Consumes an express ticket for the given node operator /// @param _nodeAddress Address of the node operator to consume express ticket for function useExpressTicket(address _nodeAddress) external override onlyLatestContract("rocketNodeManager", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) { uint256 tickets = getExpressTicketCount(_nodeAddress); require(tickets > 0, "No express tickets"); tickets -= 1; setBool(keccak256(abi.encodePacked("node.express.provisioned", _nodeAddress)), true); setUint(keccak256(abi.encodePacked("node.express.tickets", _nodeAddress)), tickets); } /// @notice Calculates a node operator's entitled express tickets and stores them /// @param _nodeAddress Address of the node operator to provision function provisionExpressTickets(address _nodeAddress) external override onlyLatestContract("rocketNodeManager", address(this)) onlyRegisteredNode(_nodeAddress) { bytes32 provisionedKey = keccak256(abi.encodePacked("node.express.provisioned", _nodeAddress)); if (getBool(provisionedKey)) { return; } uint256 tickets = getExpressTicketCount(_nodeAddress); setBool(provisionedKey, true); setUint(keccak256(abi.encodePacked("node.express.tickets", _nodeAddress)), tickets); } /// @notice Returns true if express tickets have been provisioned for the given node /// @param _nodeAddress Address of the node operator to query function getExpressTicketsProvisioned(address _nodeAddress) external override view returns (bool) { bytes32 provisionedKey = keccak256(abi.encodePacked("node.express.provisioned", _nodeAddress)); return getBool(provisionedKey); } /// @notice Refunds an express ticket for the given node operator /// @param _nodeAddress Address of the node operator to refund express ticket for function refundExpressTicket(address _nodeAddress) external override onlyLatestContract("rocketNodeManager", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) { // Refunds can only occur after the use of a ticket which guarantees tickets were provisioned addUint(keccak256(abi.encodePacked("node.express.tickets", _nodeAddress)), 1); } /// @notice Convenience function to return the megapool address for a node if it is deployed, otherwise null address /// @param _nodeAddress Address of the node to query function getMegapoolAddress(address _nodeAddress) override external view returns (address) { RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); if (rocketMegapoolFactory.getMegapoolDeployed(_nodeAddress)) { return rocketMegapoolFactory.getExpectedAddress(_nodeAddress); } return address(0x0); } /// @notice Returns the amount of unclaimed ETH rewards for a given node operator /// @param _nodeAddress Address of the node operator function getUnclaimedRewards(address _nodeAddress) external view returns (uint256) { return getUint(keccak256(abi.encodePacked("node.unclaimed.rewards", _nodeAddress))); } /// @notice Add funds to a node's unclaimed balance /// @dev Used when a withdrawal address is unable to accept ETH rewards and allows node operator to claim them later /// @param _nodeAddress Address of the node operator to increase unclaimed rewards for function addUnclaimedRewards(address _nodeAddress) external payable onlyRegisteredNode(_nodeAddress) { // Only a node's distributor can add unclaimed rewards RocketNodeDistributorFactoryInterface rocketNodeDistributorFactor = RocketNodeDistributorFactoryInterface(getContractAddress("rocketNodeDistributorFactory")); address proxyAddress = rocketNodeDistributorFactor.getProxyAddress(_nodeAddress); require(msg.sender == proxyAddress, "Only distributor can add unclaimed rewards"); // Deposit funds into vault and increase balance RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.depositEther{value: msg.value}(); addUint(keccak256(abi.encodePacked("node.unclaimed.rewards", _nodeAddress)), msg.value); // Emit event emit NodeUnclaimedRewardsAdded(_nodeAddress, msg.value, block.timestamp); } /// @notice Sends any unclaimed rewards to node operator's withdrawal address /// @param _nodeAddress Address of the node operator function claimUnclaimedRewards(address _nodeAddress) external onlyRegisteredNode(_nodeAddress) { address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); require(msg.sender == _nodeAddress || msg.sender == withdrawalAddress, "Only node can claim"); // Retrieve unclaimed rewards amount and reset balance bytes32 key = keccak256(abi.encodePacked("node.unclaimed.rewards", _nodeAddress)); uint256 amount = getUint(key); setUint(key, 0); // Withdraw ETH from vault RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.withdrawEther(amount); // Transfer to node operator's withdrawal address (bool success,) = withdrawalAddress.call{value: amount}(""); require(success, "Failed to send funds to withdrawal address"); // Emit event emit NodeUnclaimedRewardsClaimed(_nodeAddress, amount, block.timestamp); } } ================================================ FILE: contracts/contract/node/RocketNodeStaking.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketDAOProtocolSettingsNodeInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; import {RocketMinipoolManagerInterface} from "../../interface/minipool/RocketMinipoolManagerInterface.sol"; import {RocketNetworkPricesInterface} from "../../interface/network/RocketNetworkPricesInterface.sol"; import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol"; import {IERC20} from "../../interface/util/IERC20.sol"; import {IERC20Burnable} from "../../interface/util/IERC20Burnable.sol"; import {RocketBase} from "../RocketBase.sol"; /// @notice Handles staking of RPL by node operators contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface { // Immutables bytes32 immutable internal totalKey; bytes32 immutable internal totalMegapoolKey; RocketTokenRPLInterface immutable internal rplToken; RocketVaultInterface immutable internal rocketVault; // Events event RPLStaked(address indexed node, address from, uint256 amount, uint256 time); event RPLStaked(address indexed from, uint256 amount, uint256 time); event RPLUnstaked(address indexed from, uint256 amount, uint256 time); event RPLLegacyUnstaked(address indexed to, uint256 amount, uint256 time); event RPLWithdrawn(address indexed to, uint256 amount, uint256 time); event RPLSlashed(address indexed node, uint256 amount, uint256 ethValue, uint256 time); event StakeRPLForAllowed(address indexed node, address indexed caller, bool allowed, uint256 time); event RPLLockingAllowed(address indexed node, bool allowed, uint256 time); event RPLLocked(address indexed from, uint256 amount, uint256 time); event RPLUnlocked(address indexed from, uint256 amount, uint256 time); event RPLTransferred(address indexed from, address indexed to, uint256 amount, uint256 time); event RPLBurned(address indexed from, uint256 amount, uint256 time); /// @dev Reverts if not being called from a node or their RPL withdrawal address modifier onlyRPLWithdrawalAddressOrNode(address _nodeAddress) { // Check that the call is coming from RPL withdrawal address (or node if unset) RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); if (rocketNodeManager.getNodeRPLWithdrawalAddressIsSet(_nodeAddress)) { address rplWithdrawalAddress = rocketNodeManager.getNodeRPLWithdrawalAddress(_nodeAddress); require(msg.sender == rplWithdrawalAddress, "Must be called from RPL withdrawal address"); } else { require(msg.sender == _nodeAddress, "Must be called from node address"); } _; } constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 7; // Precompute keys totalKey = keccak256(abi.encodePacked("rpl.staked.total.amount")); totalMegapoolKey = keccak256(abi.encodePacked("rpl.megapool.staked.total.amount")); // Store immutable contract references rplToken = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); } /// @notice Returns the total quantity of RPL staked on the network function getTotalStakedRPL() override public view returns (uint256) { return getUint(totalKey); } /// @notice Returns the total quantity of "Megapool Staked RPL" on the network function getTotalMegapoolStakedRPL() override public view returns (uint256) { return getUint(totalMegapoolKey); } /// @notice Returns the total amount of "Legacy Staked RPL" in the protocol function getTotalLegacyStakedRPL() override external view returns (uint256) { return getTotalStakedRPL() - getTotalMegapoolStakedRPL(); } /// @notice Returns the total amount of RPL staked by a node operator (both legacy and megapool staked RPL) /// @param _nodeAddress The address of the node operator to query function getNodeStakedRPL(address _nodeAddress) override public view returns (uint256) { bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); (,, uint224 value) = rocketNetworkSnapshots.latest(key); return uint256(value); } /// @notice Returns the amount of legacy staked RPL for a node operator /// @notice _nodeAddress Address of the node operator to query function getNodeLegacyStakedRPL(address _nodeAddress) override public view returns (uint256) { bytes32 migratedKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.migrated", _nodeAddress)); if (getBool(migratedKey)) { bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress)); return getUint(legacyKey); } return getNodeStakedRPL(_nodeAddress); } /// @notice Returns the amount of megapool staked RPL for a node operator /// @notice _nodeAddress Address of the node operator to query function getNodeMegapoolStakedRPL(address _nodeAddress) override external view returns (uint256) { bytes32 migratedKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.migrated", _nodeAddress)); if (!getBool(migratedKey)) { return 0; } bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress)); return getNodeStakedRPL(_nodeAddress) - getUint(legacyKey); } /// @notice Gets the time the the given node operator's previous unstake /// @param _nodeAddress The address of the node operator to query for function getNodeLastUnstakeTime(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("rpl.megapool.unstake.time", _nodeAddress))); } /// @notice Returns the timestamp at which a node last staked RPL /// @param _nodeAddress The address of the node operator to query for function getNodeRPLStakedTime(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("rpl.staked.node.time", _nodeAddress))); } /// @notice Returns the amount of RPL that is in the "unstaking" state /// @param _nodeAddress The address of the node operator to query for function getNodeUnstakingRPL(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("rpl.megapool.unstaking.amount", _nodeAddress))); } /// @notice Returns the amount of RPL that is locked for a given node /// @param _nodeAddress The address of the node operator to query for function getNodeLockedRPL(address _nodeAddress) override public view returns (uint256) { return getUint(keccak256(abi.encodePacked("rpl.locked.node.amount", _nodeAddress))); } /// @notice Returns whether this node allows RPL locking or not /// @param _nodeAddress The address of the node operator to query for function getRPLLockingAllowed(address _nodeAddress) external view returns (bool) { return getBool(keccak256(abi.encodePacked("rpl.locking.allowed", _nodeAddress))); } /// @notice Sets the allow state for this node to perform functions that require locking RPL /// @param _nodeAddress The address of the node operator to change the state for /// @param _allowed Whether locking is allowed or not function setRPLLockingAllowed(address _nodeAddress, bool _allowed) override external onlyRPLWithdrawalAddressOrNode(_nodeAddress) { // Set the value setBool(keccak256(abi.encodePacked("rpl.locking.allowed", _nodeAddress)), _allowed); // Log it emit RPLLockingAllowed(_nodeAddress, _allowed, block.timestamp); } /// @notice Explicitly allow or remove allowance of an address to be able to stake on behalf of a node /// @dev The node operator is determined by the address calling this method, it is here for backwards compatibility /// @param _caller The address you wish to allow /// @param _allowed Whether the address is allowed or denied function setStakeRPLForAllowed(address _caller, bool _allowed) override external { setStakeRPLForAllowed(msg.sender, _caller, _allowed); } /// @notice Explicitly allow or remove allowance of an address to be able to stake on behalf of a node /// @param _nodeAddress The address of the node operator allowing the caller /// @param _caller The address you wish to allow /// @param _allowed Whether the address is allowed or denied function setStakeRPLForAllowed(address _nodeAddress, address _caller, bool _allowed) override public onlyRPLWithdrawalAddressOrNode(_nodeAddress) onlyRegisteredNode(_nodeAddress) { // Set the value setBool(keccak256(abi.encodePacked("node.stake.for.allowed", _nodeAddress, _caller)), _allowed); // Log it emit StakeRPLForAllowed(_nodeAddress, _caller, _allowed, block.timestamp); } /// @notice Increases the calling node operator's megapool staked RPL by transferring RPL from msg.sender function stakeRPL(uint256 _amount) override external onlyRegisteredNode(msg.sender) { // Check caller here and skip `stakeRPLFor` to avoid unnecessary check for rocketMerkleDistributorMainnet caller require(_callerAllowedFor(msg.sender), "Not allowed to stake for"); _stakeRPLFor(msg.sender, _amount); } /// @notice Accept an RPL stake from any address for a specified node /// Requires caller to have approved this contract to spend RPL /// Requires caller to be on the node operator's allow list (see `setStakeForAllowed`) /// @param _nodeAddress The address of the node operator to stake on behalf of /// @param _amount The amount of RPL to stake function stakeRPLFor(address _nodeAddress, uint256 _amount) override external onlyRegisteredNode(_nodeAddress) { // Must be node's RPL withdrawal address if set or the node's address or an allow listed address or rocketMerkleDistributorMainnet if (msg.sender != getAddress(keccak256(abi.encodePacked("contract.address", "rocketMerkleDistributorMainnet")))) { if (!_callerAllowedFor(_nodeAddress)) { require(getBool(keccak256(abi.encodePacked("node.stake.for.allowed", _nodeAddress, msg.sender))), "Not allowed to stake for"); } } _stakeRPLFor(_nodeAddress, _amount); } /// @dev Internal implementation for staking process function _stakeRPLFor(address _nodeAddress, uint256 _amount) internal { // Transfer RPL in and increase stake _transferRPLIn(msg.sender, _amount); _increaseNodeRPLStake(_nodeAddress, _amount); // Update last staked time _setNodeLastStakeTime(_nodeAddress); // Emit event emit RPLStaked(_nodeAddress, msg.sender, _amount, block.timestamp); } /// @notice Moves an amount of RPL from megapool staking into unstaking state /// @param _amount Amount of RPL to unstake function unstakeRPL(uint256 _amount) override external { unstakeRPLFor(msg.sender, _amount); } /// @notice Moves an amount of RPL from megapool staking into unstaking state /// @param _nodeAddress Address of node to unstake for /// @param _amount Amount of RPL to unstake function unstakeRPLFor(address _nodeAddress, uint256 _amount) override public onlyRegisteredNode(_nodeAddress) { require(_callerAllowedFor(_nodeAddress), "Not allowed to unstake for"); _unstakeRPLFor(_nodeAddress, _amount); } /// @dev Internal implementation for unstaking process function _unstakeRPLFor(address _nodeAddress, uint256 _amount) internal { // Withdraw any RPL that has been unstaking long enough _withdrawUnstakingRPL(_nodeAddress); // Move RPL from staking to unstaking _decreaseNodeMegapoolRPLStake(_nodeAddress, _amount); addUint(keccak256(abi.encodePacked("rpl.megapool.unstaking.amount", _nodeAddress)), _amount); // Reset the unstake time _setNodeLastUnstakeTime(_nodeAddress); // Emit event emit RPLUnstaked(_nodeAddress, _amount, block.timestamp); } /// @notice Withdraws any available unstaking RPL back to the node's RPL withdrawal address function withdrawRPL() override external { withdrawRPLFor(msg.sender); } /// @notice Withdraws any available unstaking RPL back to the node's RPL withdrawal address /// @param _nodeAddress Address of node to withdraw for function withdrawRPLFor(address _nodeAddress) override public onlyRegisteredNode(_nodeAddress) { require(_callerAllowedFor(_nodeAddress), "Not allowed to withdraw for"); _withdrawRPLFor(_nodeAddress); } /// @dev Internal implementation of withdrawal process function _withdrawRPLFor(address nodeAddress) internal { uint256 amount = _withdrawUnstakingRPL(nodeAddress); require(amount > 0, "No available unstaking RPL to withdraw"); emit RPLWithdrawn(nodeAddress, amount, block.timestamp); } /// @dev Withdraws any unstaking RPL back to the node operator function _withdrawUnstakingRPL(address _nodeAddress) internal returns (uint256) { // Get contracts RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); // Check unstaking period condition uint256 lastUnstakeTime = getNodeLastUnstakeTime(_nodeAddress); uint256 unstakingPeriod = rocketDAOProtocolSettingsNode.getUnstakingPeriod(); uint256 timeSinceLastUnstake = block.timestamp - lastUnstakeTime; if (timeSinceLastUnstake <= unstakingPeriod) { return 0; } // Check withdrawal cooldown if (block.timestamp - getNodeRPLStakedTime(_nodeAddress) < rocketDAOProtocolSettingsNode.getWithdrawalCooldown()) { return 0; } // Retrieve amount of RPL in unstaking state bytes32 unstakingKey = keccak256(abi.encodePacked("rpl.megapool.unstaking.amount", _nodeAddress)); uint256 amountToWithdraw = getUint(unstakingKey); if (amountToWithdraw == 0) { return 0; } // Update unstaked value setUint(unstakingKey, 0); // Perform transfer _transferRPLOut(_nodeAddress, amountToWithdraw); return amountToWithdraw; } /// @dev Unstake legacy staked RPL /// @param _amount The amount of RPL to withdraw function unstakeLegacyRPL(uint256 _amount) override external { unstakeLegacyRPLFor(msg.sender, _amount); } /// @dev Unstake legacy RPL for a given node operator /// @param _nodeAddress Address of the node operator to withdraw legacy RPL for /// @param _amount The amount of RPL to withdraw function unstakeLegacyRPLFor(address _nodeAddress, uint256 _amount) override public onlyRegisteredNode(_nodeAddress) { require(_callerAllowedFor(_nodeAddress), "Not allowed to unstake legacy RPL for"); _unstakeLegacyRPL(_nodeAddress, _amount); } /// @dev Internal implementation for legacy unstake process function _unstakeLegacyRPL(address _nodeAddress, uint256 _amount) internal { // Withdraw any RPL that has been unstaking long enough _withdrawUnstakingRPL(_nodeAddress); // Move RPL from staking to unstaking _decreaseNodeLegacyRPLStake(_nodeAddress, _amount, true); addUint(keccak256(abi.encodePacked("rpl.megapool.unstaking.amount", _nodeAddress)), _amount); // Reset the unstake time _setNodeLastUnstakeTime(_nodeAddress); // Emit event emit RPLLegacyUnstaked(_nodeAddress, _amount, block.timestamp); } /// @notice Locks an amount of RPL from being withdrawn even if the node operator is over capitalised /// @param _nodeAddress The address of the node operator /// @param _amount The amount of RPL to lock function lockRPL(address _nodeAddress, uint256 _amount) override external onlyLatestNetworkContract() { // Check status require(getBool(keccak256(abi.encodePacked("rpl.locking.allowed", _nodeAddress))), "Node is not allowed to lock RPL"); // The node must have unlocked stake equaling or greater than the amount uint256 rplStake = getNodeStakedRPL(_nodeAddress); bytes32 lockedStakeKey = keccak256(abi.encodePacked("rpl.locked.node.amount", _nodeAddress)); uint256 lockedRPL = getUint(lockedStakeKey); require(rplStake - lockedRPL >= _amount, "Not enough staked RPL"); // Increase locked RPL setUint(lockedStakeKey, lockedRPL + _amount); // Emit event emit RPLLocked(_nodeAddress, _amount, block.timestamp); } /// @notice Unlocks an amount of RPL making it possible to withdraw if the nod is over capitalised /// @param _nodeAddress The address of the node operator /// @param _amount The amount of RPL to unlock function unlockRPL(address _nodeAddress, uint256 _amount) override external onlyLatestNetworkContract() { // The node must have locked stake equaling or greater than the amount bytes32 lockedStakeKey = keccak256(abi.encodePacked("rpl.locked.node.amount", _nodeAddress)); uint256 lockedRPL = getUint(lockedStakeKey); require(_amount <= lockedRPL, "Not enough locked RPL"); // Decrease locked RPL setUint(lockedStakeKey, lockedRPL - _amount); // Emit event emit RPLUnlocked(_nodeAddress, _amount, block.timestamp); } /// @notice Transfers RPL from one node to another /// @param _from The node to transfer from /// @param _to The node to transfer to /// @param _amount The amount of RPL to transfer function transferRPL(address _from, address _to, uint256 _amount) override external onlyLatestNetworkContract() onlyRegisteredNode(_from) onlyRegisteredNode(_to) { // Check sender has enough RPL require(getNodeStakedRPL(_from) >= _amount, "Sender has insufficient RPL"); require(_from != _to, "Cannot transfer to same address"); // Transfer the stake _decreaseNodeRPLStake(_from, _amount); _increaseNodeRPLStake(_to, _amount); // Emit event emit RPLTransferred(_from, _to, _amount, block.timestamp); } /// @notice Burns an amount of RPL staked by a given node operator /// @param _from The node to burn from /// @param _amount The amount of RPL to burn function burnRPL(address _from, uint256 _amount) override external onlyLatestNetworkContract() onlyRegisteredNode(_from) { // Check sender has enough RPL require(getNodeStakedRPL(_from) >= _amount, "Node has insufficient RPL"); // Decrease the stake amount _decreaseNodeRPLStake(_from, _amount); // Withdraw the RPL to this contract rocketVault.withdrawToken(address(this), rplToken, _amount); // Execute the token burn IERC20Burnable(address(rplToken)).burn(_amount); // Emit event emit RPLBurned(_from, _amount, block.timestamp); } /// @notice Slash a node's legacy RPL by an ETH amount /// Only accepts calls from registered minipools /// @param _nodeAddress The address to slash RPL from /// @param _ethSlashAmount The amount of RPL to slash denominated in ETH value function slashRPL(address _nodeAddress, uint256 _ethSlashAmount) override external onlyRegisteredMinipool(msg.sender) { // Load contracts RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices")); // Calculate RPL amount to slash uint256 rplSlashAmount = calcBase * _ethSlashAmount / rocketNetworkPrices.getRPLPrice(); // Cap slashed amount to node's RPL stake uint256 rplStake = getNodeLegacyStakedRPL(_nodeAddress); if (rplSlashAmount > rplStake) {rplSlashAmount = rplStake;} // Transfer slashed amount to auction contract if (rplSlashAmount > 0) rocketVault.transferToken("rocketAuctionManager", IERC20(getContractAddress("rocketTokenRPL")), rplSlashAmount); // Update RPL stake amounts _decreaseNodeLegacyRPLStake(_nodeAddress, rplSlashAmount, false); // Mark minipool as slashed setBool(keccak256(abi.encodePacked("minipool.rpl.slashed", msg.sender)), true); // Emit RPL slashed event emit RPLSlashed(_nodeAddress, rplSlashAmount, _ethSlashAmount, block.timestamp); } /// @dev Increases a node operator's megapool staked RPL amount /// @param _amount How much to increase staked RPL by function _increaseNodeRPLStake(address _nodeAddress, uint256 _amount) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); (,, uint224 value) = rocketNetworkSnapshots.latest(key); _migrateLegacy(_nodeAddress, uint256(value)); rocketNetworkSnapshots.push(key, value + uint224(_amount)); // Increase total addUint(totalKey, _amount); addUint(totalMegapoolKey, _amount); } /// @dev Decreases a node operator's staked RPL, first taking from their legacy than their megapool staked RPL /// Does not check conditions for minimum stake requirements function _decreaseNodeRPLStake(address _nodeAddress, uint256 _amount) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); (,, uint224 totalStakedRPL) = rocketNetworkSnapshots.latest(key); _migrateLegacy(_nodeAddress, uint256(totalStakedRPL)); // Take from megapool amount if not enough legacy uint256 legacyStakedRPL = getNodeLegacyStakedRPL(_nodeAddress); uint256 legacyAmount = _amount; if (legacyAmount > legacyStakedRPL) { uint256 megapoolAmount = legacyAmount - legacyStakedRPL; legacyAmount -= megapoolAmount; // Decrease megapool total subUint(totalMegapoolKey, megapoolAmount); } // Store new values if (legacyAmount > 0) { bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress)); subUint(legacyKey, legacyAmount); } rocketNetworkSnapshots.push(key, totalStakedRPL - uint224(_amount)); // Decrease total subUint(totalKey, _amount); } /// @dev Decreases a node operator's megapool staked RPL amount /// @param _nodeAddress Address of node to decrease megapool staked RPL for /// @param _amount Amount to decrease by function _decreaseNodeMegapoolRPLStake(address _nodeAddress, uint256 _amount) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); (,, uint224 totalStakedRPL) = rocketNetworkSnapshots.latest(key); _migrateLegacy(_nodeAddress, uint256(totalStakedRPL)); // Check node operator has sufficient RPL to reduce uint256 legacyStakedRPL = getNodeLegacyStakedRPL(_nodeAddress); uint256 lockedRPL = getNodeLockedRPL(_nodeAddress); require( uint256(totalStakedRPL) >= _amount + lockedRPL && uint256(totalStakedRPL) >= _amount + legacyStakedRPL, "Insufficient RPL stake to reduce" ); // Store new value rocketNetworkSnapshots.push(key, totalStakedRPL - uint224(_amount)); // Decrease totals subUint(totalKey, _amount); subUint(totalMegapoolKey, _amount); } /// @dev Decreases a node operator's legacy staked RPL amount /// @param _nodeAddress Address of node to decrease legacy staked RPL for /// @param _amount Amount to decrease by /// @param _ensureMinimums Whether to assert the NO has the minimum required legacy RPL stake and sufficient unlocked RPL for the decrease function _decreaseNodeLegacyRPLStake(address _nodeAddress, uint256 _amount, bool _ensureMinimums) internal { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress)); (,, uint224 totalStakedRPL) = rocketNetworkSnapshots.latest(key); _migrateLegacy(_nodeAddress, uint256(totalStakedRPL)); // Compute legacy staked RPL storage key bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress)); if (_ensureMinimums) { // Check amount after decrease does not fall below minimum required uint256 legacyStakedRPL = getUint(legacyKey); uint256 minimumLegacyStakedRPL = getNodeMinimumLegacyRPLStake(_nodeAddress); require( legacyStakedRPL >= _amount + minimumLegacyStakedRPL, "Insufficient legacy staked RPL" ); // Check node has enough unlocked RPL for the reduction uint256 lockedRPL = getNodeLockedRPL(_nodeAddress); require( uint256(totalStakedRPL) >= _amount + lockedRPL, "Insufficient RPL stake to reduce" ); } // Store new values rocketNetworkSnapshots.push(key, totalStakedRPL - uint224(_amount)); subUint(legacyKey, _amount); // Decrease total subUint(totalKey, _amount); } /// @notice Returns the total amount of a node operator's bonded ETH (minipool + megapool) /// @param _nodeAddress Address of the node operator to query function getNodeETHBonded(address _nodeAddress) public view returns (uint256) { return getNodeMegapoolETHBonded(_nodeAddress) + getNodeMinipoolETHBonded(_nodeAddress); } /// @notice Returns the amount of a node operator's megapool bonded ETH /// @param _nodeAddress Address of the node operator to query function getNodeMegapoolETHBonded(address _nodeAddress) public view returns (uint256) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("megapool.eth.provided.node.amount", _nodeAddress)); (, , uint224 value) = rocketNetworkSnapshots.latest(key); return uint256(value); } /// @notice Returns the amount of a node operator's minipool bonded ETH /// @param _nodeAddress Address of the node operator to query function getNodeMinipoolETHBonded(address _nodeAddress) public view returns (uint256) { // Get contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); uint256 activeMinipoolCount = rocketMinipoolManager.getNodeActiveMinipoolCount(_nodeAddress); // Retrieve stored ETH borrowed value uint256 ethBorrowed = getNodeMinipoolETHBorrowed(_nodeAddress); // ETH bonded is number of staking minipools * 32 - eth borrowed uint256 totalEthStaked = activeMinipoolCount * 32 ether; return totalEthStaked - ethBorrowed; } /// @notice Returns a node's borrowed ETH amount /// @param _nodeAddress The address of the node operator to query function getNodeMegapoolETHBorrowed(address _nodeAddress) public view returns (uint256) { bytes32 key = keccak256(abi.encodePacked("megapool.eth.matched.node.amount", _nodeAddress)); return getUint(key); } /// @notice Returns a node's borrowed ETH amount /// @param _nodeAddress The address of the node operator to query function getNodeMinipoolETHBorrowed(address _nodeAddress) public view returns (uint256) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); bytes32 key = keccak256(abi.encodePacked("eth.matched.node.amount", _nodeAddress)); (,,uint224 value) = rocketNetworkSnapshots.latest(key); return value; } /// @notice Returns a node's total borrowed ETH amount (minipool + megapool) /// @param _nodeAddress The address of the node operator to query function getNodeETHBorrowed(address _nodeAddress) public view returns (uint256) { return getNodeMegapoolETHBorrowed(_nodeAddress) + getNodeMinipoolETHBorrowed(_nodeAddress); } /// @notice Returns the ratio between capital taken from users and bonded by a node operator for minipools /// The value is a 1e18 precision fixed point integer value of (node capital + user capital) / node capital. /// @param _nodeAddress The address of the node operator to query /// @dev Inconsistent naming for backwards compatibility function getNodeETHCollateralisationRatio(address _nodeAddress) public view returns (uint256) { uint256 borrowedETH = getNodeMinipoolETHBorrowed(_nodeAddress); uint256 bondedETH = getNodeMinipoolETHBonded(_nodeAddress); if (borrowedETH == 0 || bondedETH == 0) { return calcBase * 2; } uint256 ethTotal = borrowedETH + bondedETH; return (ethTotal * calcBase) / (ethTotal - borrowedETH); } /// @notice Returns the minimum amount of legacy staked RPL a node must have after unstaking /// @param _nodeAddress The address of the node operator to calculate for function getNodeMinimumLegacyRPLStake(address _nodeAddress) public view returns (uint256) { // Load contracts RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices")); RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); // Calculate and return minimum uint256 minimumRPLStakePercent = rocketDAOProtocolSettingsNode.getMinimumLegacyRPLStake(); uint256 borrowedETH = getNodeMinipoolETHBorrowed(_nodeAddress); return borrowedETH * minimumRPLStakePercent / rocketNetworkPrices.getRPLPrice(); } /// @dev If legacy RPL balance has not been migrated, migrate it. Otherwise, do nothing function _migrateLegacy(address _nodeAddress, uint256 _amount) internal { bytes32 migratedKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.migrated", _nodeAddress)); if (getBool(migratedKey)) { return; } bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress)); setUint(legacyKey, _amount); setBool(migratedKey, true); } /// @dev Transfers RPL out of vault into node's withdrawal address function _transferRPLOut(address _nodeAddress, uint256 _amount) internal { RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); address rplWithdrawalAddress = rocketNodeManager.getNodeRPLWithdrawalAddress(_nodeAddress); rocketVault.withdrawToken(rplWithdrawalAddress, IERC20(getContractAddress("rocketTokenRPL")), _amount); } /// @dev Transfers RPL from msg.sender into vault function _transferRPLIn(address _nodeAddress, uint256 _amount) internal { // Transfer RPL tokens require(rplToken.transferFrom(_nodeAddress, address(this), _amount), "Could not transfer RPL to staking contract"); // Deposit RPL tokens to vault require(rplToken.approve(address(rocketVault), _amount), "Could not approve vault RPL deposit"); rocketVault.depositToken("rocketNodeStaking", rplToken, _amount); } /// @dev Sets the time of the given node operator's unstake to the current block time function _setNodeLastUnstakeTime(address _nodeAddress) internal { setUint(keccak256(abi.encodePacked("rpl.megapool.unstake.time", _nodeAddress)), block.timestamp); } /// @dev Sets the time of the given node operator's stake to the current block time function _setNodeLastStakeTime(address _nodeAddress) internal { setUint(keccak256(abi.encodePacked("rpl.staked.node.time", _nodeAddress)), block.timestamp); } /// @dev Implements caller restrictions (per RPIP-31): /// - If a node’s RPL withdrawal address is unset, the call MUST come from one of: the node’s primary withdrawal address, or the node’s address /// - If a node’s RPL withdrawal address is set, the call MUST come from the current RPL withdrawal address function _callerAllowedFor(address _nodeAddress) internal view returns (bool) { RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); if (rocketNodeManager.getNodeRPLWithdrawalAddressIsSet(_nodeAddress)) { address rplWithdrawalAddress = rocketNodeManager.getNodeRPLWithdrawalAddress(_nodeAddress); return msg.sender == rplWithdrawalAddress; } else { address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); return (msg.sender == _nodeAddress) || (msg.sender == withdrawalAddress); } } } ================================================ FILE: contracts/contract/rewards/RocketClaimDAO.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketBase} from "../RocketBase.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketRewardsPoolInterface} from "../../interface/rewards/RocketRewardsPoolInterface.sol"; import {RocketClaimDAOInterface} from "../../interface/rewards/claims/RocketClaimDAOInterface.sol"; import {IERC20} from "../../interface/util/IERC20.sol"; /// @notice Recipient of pDAO RPL from inflation. Performs treasury spends and handles recurring payments. contract RocketClaimDAO is RocketBase, RocketClaimDAOInterface { // Offsets into storage for contract details uint256 constant internal existsOffset = 0; uint256 constant internal recipientOffset = 1; uint256 constant internal amountOffset = 2; uint256 constant internal periodLengthOffset = 3; uint256 constant internal lastPaymentOffset = 4; uint256 constant internal numPeriodsOffset = 5; uint256 constant internal periodsPaidOffset = 6; // Events event RPLTokensSentByDAOProtocol(string indexed invoiceID, address indexed from, address indexed to, uint256 amount, uint256 time); event RPLTreasuryContractPayment(string indexed contractName, address indexed recipient, uint256 amount, uint256 time); event RPLTreasuryContractClaimed(address indexed recipient, uint256 amount, uint256 time); event RPLTreasuryContractCreated(string indexed contractName, address indexed recipient, uint256 amountPerPeriod, uint256 startTime, uint256 periodLength, uint256 numPeriods); event RPLTreasuryContractUpdated(string indexed contractName, address indexed recipient, uint256 amountPerPeriod, uint256 periodLength, uint256 numPeriods); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 4; } /// @dev Receive pDAO share of rewards from megapool distributions and reward submissions receive() payable external { // Transfer incoming ETH directly to the vault RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.depositEther{value: msg.value}(); // Note: There is currently no way to spend this ETH } /// @notice Returns whether a contract with the given name exists /// @param _contractName Name of the contract to check existence of function getContractExists(string calldata _contractName) external view returns (bool) { uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractName))); return getBool(bytes32(contractKey + existsOffset)); } /// @notice Gets details about a given payment contract /// @param _contractName Name of the contract to retrieve details for function getContract(string calldata _contractName) override external view returns (PaymentContract memory) { // Compute key uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractName))); // Retrieve details PaymentContract memory paymentContract; paymentContract.recipient = getAddress(bytes32(contractKey + recipientOffset)); paymentContract.amountPerPeriod = getUint(bytes32(contractKey + amountOffset)); paymentContract.periodLength = getUint(bytes32(contractKey + periodLengthOffset)); paymentContract.lastPaymentTime = getUint(bytes32(contractKey + lastPaymentOffset)); paymentContract.numPeriods = getUint(bytes32(contractKey + numPeriodsOffset)); paymentContract.periodsPaid = getUint(bytes32(contractKey + periodsPaidOffset)); return paymentContract; } /// @notice Gets the outstanding balance owed to a given recipient /// @param _recipientAddress The address of the recipient to return the balance of function getBalance(address _recipientAddress) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("dao.protocol.treasury.balance", _recipientAddress))); } /// @notice Spend the network DAOs RPL rewards /// @param _invoiceID A string used to identify this payment (not used internally) /// @param _recipientAddress The address to send the RPL spend to /// @param _amount The amount of RPL to send function spend(string memory _invoiceID, address _recipientAddress, uint256 _amount) override external onlyLatestContract("rocketDAOProtocolProposals", msg.sender) onlyLatestContract("rocketClaimDAO", address(this)) { // Load contracts RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Addresses IERC20 rplToken = IERC20(getContractAddress("rocketTokenRPL")); // Some initial checks require(_amount > 0 && _amount <= rocketVault.balanceOfToken("rocketClaimDAO", rplToken), "You cannot send 0 RPL or more than the DAO has in its account"); // Send now rocketVault.withdrawToken(_recipientAddress, rplToken, _amount); // Log it emit RPLTokensSentByDAOProtocol(_invoiceID, address(this), _recipientAddress, _amount, block.timestamp); } /// @notice Creates a new recurring payment contract /// @param _contractName A string used to identify this payment /// @param _recipientAddress The address which can claim against this recurring payment /// @param _amountPerPeriod The amount of RPL that can be claimed each period /// @param _periodLength The length (in seconds) of periods of this contract /// @param _startTime A unix timestamp of when payments begin /// @param _numPeriods The number of periods this contract pays out for function newContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) override external onlyLatestContract("rocketDAOProtocolProposals", msg.sender) onlyLatestContract("rocketClaimDAO", address(this)) { uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractName))); // Ensure contract name uniqueness require(getBool(bytes32(contractKey + existsOffset)) == false, "Contract already exists"); // Write to storage setBool(bytes32(contractKey + existsOffset), true); setAddress(bytes32(contractKey + recipientOffset), _recipientAddress); setUint(bytes32(contractKey + amountOffset), _amountPerPeriod); setUint(bytes32(contractKey + periodLengthOffset), _periodLength); setUint(bytes32(contractKey + lastPaymentOffset), _startTime); setUint(bytes32(contractKey + numPeriodsOffset), _numPeriods); // setUint(bytes32(contractKey + periodsPaidOffset), 0); // Log it emit RPLTreasuryContractCreated(_contractName, _recipientAddress, _amountPerPeriod, _startTime, _periodLength, _numPeriods); } /// @notice Modifies an existing recurring payment contract /// @param _contractName The contract to modify /// @param _recipientAddress The address which can claim against this recurring payment /// @param _amountPerPeriod The amount of RPL that can be claimed each period /// @param _periodLength The length (in seconds) of periods of this contract /// @param _numPeriods The number of periods this contract pays out for function updateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) override external onlyLatestContract("rocketDAOProtocolProposals", msg.sender) onlyLatestContract("rocketClaimDAO", address(this)) { uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractName))); // Check it exists require(getBool(bytes32(contractKey + existsOffset)) == true, "Contract does not exist"); // Write to storage uint256 lastPaymentTime = getUint(bytes32(contractKey + lastPaymentOffset)); // Payout contract per existing parameters if contract has already started if (block.timestamp > lastPaymentTime) { _payOutContract(_contractName); } // Update the contract setAddress(bytes32(contractKey + recipientOffset), _recipientAddress); setUint(bytes32(contractKey + amountOffset), _amountPerPeriod); setUint(bytes32(contractKey + periodLengthOffset), _periodLength); setUint(bytes32(contractKey + numPeriodsOffset), _numPeriods); // Log it emit RPLTreasuryContractUpdated(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _numPeriods); } /// @notice Can be called to withdraw any paid amounts of RPL to the supplied recipient /// @param _recipientAddress The recipient address to claim for function withdrawBalance(address _recipientAddress) override public onlyLatestContract("rocketClaimDAO", address(this)) { // Load contracts RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Addresses IERC20 rplToken = IERC20(getContractAddress("rocketTokenRPL")); // Get pending balance bytes32 balanceKey = keccak256(abi.encodePacked("dao.protocol.treasury.balance", _recipientAddress)); uint256 amount = getUint(balanceKey); // Zero out pending balance setUint(balanceKey, 0); // Some initial checks require(amount > 0, "No balance to withdraw"); require(amount <= rocketVault.balanceOfToken("rocketClaimDAO", rplToken), "Insufficient treasury balance for withdrawal"); // Send now rocketVault.withdrawToken(_recipientAddress, rplToken, amount); // Log it emit RPLTreasuryContractClaimed(_recipientAddress, amount, block.timestamp); } /// @notice Executes payout on the given contracts /// @param _contractNames An array of contract names to execute a payout on function payOutContracts(string[] calldata _contractNames) override external onlyLatestContract("rocketClaimDAO", address(this)) { uint256 contractNamesLength = _contractNames.length; for (uint256 i = 0; i < contractNamesLength; ++i) { _payOutContract(_contractNames[i]); } } /// @notice Executes a payout on given contracts and withdraws the resulting balance to the recipient /// @param _contractNames An array of contract names to execute a payout and withdraw on function payOutContractsAndWithdraw(string[] calldata _contractNames) override external onlyLatestContract("rocketClaimDAO", address(this)) { uint256 contractNamesLength = _contractNames.length; for (uint256 i = 0; i < contractNamesLength; ++i) { // Payout contract _payOutContract(_contractNames[i]); // Withdraw to contract recipient uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractNames[i]))); address recipient = getAddress(bytes32(contractKey + recipientOffset)); withdrawBalance(recipient); } } /// @dev Pays out any outstanding amounts to the recipient of a contract function _payOutContract(string memory _contractName) internal { // Load contracts RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); IERC20 rplToken = IERC20(getContractAddress("rocketTokenRPL")); // Check contract exists uint256 contractKey = uint256(keccak256(abi.encodePacked("dao.protocol.treasury.contract", _contractName))); require(getBool(bytes32(contractKey + existsOffset)) == true, "Contract does not exist"); // Get time of last payout uint256 lastPaymentTime = getUint(bytes32(contractKey + lastPaymentOffset)); // Payments haven't started yet (nothing to do) if (block.timestamp < lastPaymentTime) { return; } // Calculate how many periods have passed uint256 periodLength = getUint(bytes32(contractKey + periodLengthOffset)); uint256 periodsToPay = (block.timestamp - lastPaymentTime) / periodLength; uint256 periodsPaid = getUint(bytes32(contractKey + periodsPaidOffset)); uint256 numPeriods = getUint(bytes32(contractKey + numPeriodsOffset)); // Calculate how many periods to pay if (periodsToPay + periodsPaid > numPeriods) { periodsToPay = numPeriods - periodsPaid; } // Check if already paid up to date if (periodsToPay == 0) { return; } // Calculate how much to pay for unpaid periods address recipientAddress = getAddress(bytes32(contractKey + recipientOffset)); uint256 amountPerPeriod = getUint(bytes32(contractKey + amountOffset)); uint256 amountToPay = periodsToPay * amountPerPeriod; // Check for adequate vault balance require(amountToPay <= rocketVault.balanceOfToken("rocketClaimDAO", rplToken), "Insufficient treasury balance for payout"); // Update last paid timestamp and periods paid setUint(bytes32(contractKey + lastPaymentOffset), lastPaymentTime + (periodsToPay * periodLength)); setUint(bytes32(contractKey + periodsPaidOffset), periodsPaid + periodsToPay); // Add to the recipient's balance addUint(keccak256(abi.encodePacked("dao.protocol.treasury.balance", recipientAddress)), amountToPay); // Emit event emit RPLTreasuryContractPayment(_contractName, recipientAddress, amountToPay, block.timestamp); } } ================================================ FILE: contracts/contract/rewards/RocketMerkleDistributorMainnet.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol"; import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol"; import {RocketMerkleDistributorMainnetInterface} from "../../interface/rewards/RocketMerkleDistributorMainnetInterface.sol"; import {Claim} from "../../interface/rewards/RocketRewardsRelayInterface.sol"; import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol"; import {IERC20} from "../../interface/util/IERC20.sol"; import {RocketBase} from "../RocketBase.sol"; import {MerkleProof} from "@openzeppelin4/contracts/utils/cryptography/MerkleProof.sol"; /// @dev On mainnet, the relay and the distributor are the same contract as there is no need for an intermediate contract to /// handle cross-chain messaging. contract RocketMerkleDistributorMainnet is RocketBase, RocketMerkleDistributorMainnetInterface, RocketVaultWithdrawerInterface { // Events event RewardsClaimed(address indexed claimer, Claim[] claims); // Constants uint256 constant network = 0; // Immutables bytes32 immutable rocketVaultKey; bytes32 immutable rocketTokenRPLKey; // Allow receiving ETH receive() payable external {} // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 3; // Precompute keys rocketVaultKey = keccak256(abi.encodePacked("contract.address", "rocketVault")); rocketTokenRPLKey = keccak256(abi.encodePacked("contract.address", "rocketTokenRPL")); } /// @notice Used following an upgrade or new deployment to initialise the relay function initialise() external override { // On new deploy, allow guardian to initialise, otherwise, only a network contract if (rocketStorage.getDeployedStatus()) { require(getBool(keccak256(abi.encodePacked("contract.exists", msg.sender))), "Invalid or outdated network contract"); } else { require(msg.sender == rocketStorage.getGuardian(), "Not guardian"); } // Set this contract as the relay for network 0 setAddress(keccak256(abi.encodePacked("rewards.relay.address", uint256(0))), address(this)); } /// @notice Called by RocketRewardsPool to include a snapshot into this distributor function relayRewards(uint256 _rewardIndex, uint256 _treeVersion, bytes32 _root, uint256 _rewardsRPL, uint256 _rewardsETH) external override onlyLatestContract("rocketMerkleDistributorMainnet", address(this)) onlyLatestContract("rocketRewardsPool", msg.sender) { // Store root bytes32 key = keccak256(abi.encodePacked('rewards.merkle.root', _rewardIndex)); require(getBytes32(key) == bytes32(0)); setBytes32(key, _root); // Store tree version bytes32 versionKey = keccak256(abi.encodePacked('rewards.interval.tree.version', _rewardIndex)); setUint(versionKey, _treeVersion); // Send the ETH and RPL to the vault RocketVaultInterface rocketVault = RocketVaultInterface(getAddress(rocketVaultKey)); if (_rewardsETH > 0) { rocketVault.depositEther{value: _rewardsETH}(); } if (_rewardsRPL > 0) { IERC20 rocketTokenRPL = IERC20(getAddress(rocketTokenRPLKey)); rocketTokenRPL.approve(address(rocketVault), _rewardsRPL); rocketVault.depositToken("rocketMerkleDistributorMainnet", rocketTokenRPL, _rewardsRPL); } } /// @notice Reward recipients can call this method with a merkle proof to claim rewards for one or more reward intervals function claim(address _nodeAddress, Claim[] calldata _claims) external override { claimAndStake(_nodeAddress, _claims, 0); } /// @notice Reward recipients can call this method to claim rewards for one or more reward intervals and immediately stake some or all of the claimed RPL function claimAndStake(address _nodeAddress, Claim[] calldata _claims, uint256 _stakeAmount) public override { _verifyClaim(_nodeAddress, _claims); _claimAndStake(_nodeAddress, _claims, _stakeAmount); } /// @notice Node operators can call this method to claim rewards for one or more reward intervals and specify an amount of RPL to stake at the same time function _claimAndStake(address _nodeAddress, Claim[] calldata _claims, uint256 _stakeAmount) internal { // Get contracts RocketVaultInterface rocketVault = RocketVaultInterface(getAddress(rocketVaultKey)); address rplWithdrawalAddress; address withdrawalAddress; // Confirm caller is permitted { RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); rplWithdrawalAddress = rocketNodeManager.getNodeRPLWithdrawalAddress(_nodeAddress); withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); if (rocketNodeManager.getNodeRPLWithdrawalAddressIsSet(_nodeAddress)) { if (_stakeAmount > 0) { // If staking and RPL withdrawal address is set, must be called from RPL withdrawal address require(msg.sender == rplWithdrawalAddress, "Can only claim and stake from RPL withdrawal address"); } else { // Otherwise, must be called from RPL withdrawal address, node address or withdrawal address require(msg.sender == rplWithdrawalAddress || msg.sender == _nodeAddress || msg.sender == withdrawalAddress, "Can only claim from withdrawal addresses or node address"); } } else { // If RPL withdrawal address isn't set, must be called from node address or withdrawal address require(msg.sender == _nodeAddress || msg.sender == withdrawalAddress, "Can only claim from node address"); } } address rocketTokenRPLAddress = getAddress(rocketTokenRPLKey); // Calculate totals { uint256 totalAmountRPL = 0; uint256 totalAmountSmoothingPoolETH = 0; uint256 totalAmountVoterETH = 0; for (uint256 i = 0; i < _claims.length; ++i) { totalAmountRPL = totalAmountRPL + _claims[i].amountRPL; totalAmountSmoothingPoolETH = totalAmountSmoothingPoolETH + _claims[i].amountSmoothingPoolETH; totalAmountVoterETH = totalAmountVoterETH + _claims[i].amountVoterETH; } // Validate input require(_stakeAmount <= totalAmountRPL, "Invalid stake amount"); { // Distribute any remaining tokens to the node's withdrawal address uint256 remaining = totalAmountRPL - _stakeAmount; if (remaining > 0) { rocketVault.withdrawToken(rplWithdrawalAddress, IERC20(rocketTokenRPLAddress), remaining); } } // Distribute ETH if (totalAmountSmoothingPoolETH + totalAmountVoterETH > 0) { rocketVault.withdrawEther(totalAmountSmoothingPoolETH + totalAmountVoterETH); if (totalAmountSmoothingPoolETH > 0) { // Allow up to 10000 gas to send ETH to the withdrawal address (bool result,) = withdrawalAddress.call{value: totalAmountSmoothingPoolETH, gas: 10000}(""); if (!result) { // If the withdrawal address cannot accept the ETH with 10000 gas, add it to their balance to be claimed later at their own expense bytes32 balanceKey = keccak256(abi.encodePacked('rewards.eth.balance', withdrawalAddress)); addUint(balanceKey, totalAmountSmoothingPoolETH); // Return the ETH to the vault rocketVault.depositEther{value: totalAmountSmoothingPoolETH}(); } } if (totalAmountVoterETH > 0) { // Allow up to 10000 gas to send ETH to the RPL withdrawal address (bool result,) = rplWithdrawalAddress.call{value: totalAmountVoterETH, gas: 10000}(""); if (!result) { // If the RPL withdrawal address cannot accept the ETH with 10000 gas, add it to their balance to be claimed later at their own expense bytes32 balanceKey = keccak256(abi.encodePacked('rewards.eth.balance', rplWithdrawalAddress)); addUint(balanceKey, totalAmountVoterETH); // Return the ETH to the vault rocketVault.depositEther{value: totalAmountVoterETH}(); } } } } // Restake requested amount if (_stakeAmount > 0) { RocketTokenRPLInterface rocketTokenRPL = RocketTokenRPLInterface(rocketTokenRPLAddress); RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketVault.withdrawToken(address(this), IERC20(rocketTokenRPLAddress), _stakeAmount); rocketTokenRPL.approve(address(rocketNodeStaking), _stakeAmount); rocketNodeStaking.stakeRPLFor(_nodeAddress, _stakeAmount); } // Emit event emit RewardsClaimed(_nodeAddress, _claims); } /// @notice If ETH was claimed but was unable to be sent to the withdrawal address, it can be claimed via this function function claimOutstandingEth() external override { // Get contracts RocketVaultInterface rocketVault = RocketVaultInterface(getAddress(rocketVaultKey)); // Get the amount and zero it out bytes32 balanceKey = keccak256(abi.encodePacked('rewards.eth.balance', msg.sender)); uint256 amount = getUint(balanceKey); setUint(balanceKey, 0); // Withdraw the ETH from the vault rocketVault.withdrawEther(amount); // Attempt to send it to the caller (bool result,) = payable(msg.sender).call{value: amount}(""); require(result, 'Transfer failed'); } /// @notice Returns the amount of ETH that can be claimed by a withdrawal address function getOutstandingEth(address _address) external override view returns (uint256) { bytes32 balanceKey = keccak256(abi.encodePacked('rewards.eth.balance', _address)); return getUint(balanceKey); } /// @notice Verifies the given data exists as a leaf nodes for the specified reward interval and marks them as claimed if they are valid /// @dev This function is optimised for gas when _rewardIndex is ordered numerically function _verifyClaim(address _nodeAddress, Claim[] calldata _claim) internal { // Set initial parameters to the first reward index in the array uint256 indexWordIndex = _claim[0].rewardIndex / 256; bytes32 claimedWordKey = keccak256(abi.encodePacked('rewards.interval.claimed', _nodeAddress, indexWordIndex)); uint256 claimedWord = getUint(claimedWordKey); // Loop over every entry for (uint256 i = 0; i < _claim.length; ++i) { // Prevent accidental claim of 0 require(_claim[i].amountRPL > 0 || _claim[i].amountSmoothingPoolETH > 0 || _claim[i].amountVoterETH > 0, "Invalid amount"); // Check if this entry has a different word index than the previous if (indexWordIndex != _claim[i].rewardIndex / 256) { // Store the previous word setUint(claimedWordKey, claimedWord); // Load the word for this entry indexWordIndex = _claim[i].rewardIndex / 256; claimedWordKey = keccak256(abi.encodePacked('rewards.interval.claimed', _nodeAddress, indexWordIndex)); claimedWord = getUint(claimedWordKey); } // Calculate the bit index for this entry uint256 indexBitIndex = _claim[i].rewardIndex % 256; // Ensure the bit is not yet set on this word uint256 mask = (1 << indexBitIndex); require(claimedWord & mask != mask, "Already claimed"); // Verify the merkle proof require(_verifyProof(_nodeAddress, _claim[i]), "Invalid proof"); // Set the bit for the current reward index claimedWord = claimedWord | (1 << indexBitIndex); } // Store the word setUint(claimedWordKey, claimedWord); } /// @notice Verifies that the given proof is valid function _verifyProof(address _nodeAddress, Claim calldata _claim) internal view returns (bool) { bytes32 versionKey = keccak256(abi.encodePacked('rewards.interval.tree.version', _claim.rewardIndex)); uint256 version = getUint(versionKey); if (version == 0) { // v0 did not include `amountVoterETH`, so ensure the value supplied is 0 require(_claim.amountVoterETH == 0, "Invalid claim"); bytes32 node = keccak256(abi.encodePacked(_nodeAddress, network, _claim.amountRPL, _claim.amountSmoothingPoolETH)); bytes32 key = keccak256(abi.encodePacked('rewards.merkle.root', _claim.rewardIndex)); bytes32 merkleRoot = getBytes32(key); return MerkleProof.verify(_claim.merkleProof, merkleRoot, node); } else if(version == 1) { bytes32 node = keccak256(abi.encodePacked(_nodeAddress, network, _claim.amountRPL, _claim.amountSmoothingPoolETH, _claim.amountVoterETH)); bytes32 key = keccak256(abi.encodePacked('rewards.merkle.root', _claim.rewardIndex)); bytes32 merkleRoot = getBytes32(key); return MerkleProof.verify(_claim.merkleProof, merkleRoot, node); } revert("Invalid version"); } /// @notice Returns true if the given claimer has claimed for the given reward interval function isClaimed(uint256 _rewardIndex, address _claimer) public override view returns (bool) { uint256 indexWordIndex = _rewardIndex / 256; uint256 indexBitIndex = _rewardIndex % 256; uint256 claimedWord = getUint(keccak256(abi.encodePacked('rewards.interval.claimed', _claimer, indexWordIndex))); uint256 mask = (1 << indexBitIndex); return claimedWord & mask == mask; } /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyLatestContract("rocketMerkleDistributorMainnet", address(this)) onlyLatestContract("rocketVault", msg.sender) {} } ================================================ FILE: contracts/contract/rewards/RocketRewardsPool.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol"; import {RocketDAONodeTrustedInterface} from "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import {RocketDAOProtocolSettingsNetworkInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; import {RocketDAOProtocolSettingsRewardsInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol"; import {RocketRewardsPoolInterface} from "../../interface/rewards/RocketRewardsPoolInterface.sol"; import {RocketRewardsRelayInterface} from "../../interface/rewards/RocketRewardsRelayInterface.sol"; import {RocketSmoothingPoolInterface} from "../../interface/rewards/RocketSmoothingPoolInterface.sol"; import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol"; import {IERC20} from "../../interface/util/IERC20.sol"; import {RewardSubmission} from "../../types/RewardSubmission.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; /// @notice Holds RPL and ETH generated by the network for distribution each reward cycle contract RocketRewardsPool is RocketBase, RocketRewardsPoolInterface, RocketVaultWithdrawerInterface { // Constants uint256 constant internal treeVersion = 1; // Events event RewardSnapshotSubmitted(address indexed from, uint256 indexed rewardIndex, RewardSubmission submission, uint256 time); event RewardSnapshot(uint256 indexed rewardIndex, RewardSubmission submission, uint256 intervalStartTime, uint256 intervalEndTime, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 5; } /// @dev Needs to freely accept ETH withdrawn from the smoothing pool receive() payable external {} /// @dev Callback required to receive ETH withdrawal from the vault function receiveVaultWithdrawalETH() override external payable onlyLatestContract("rocketRewardsPool", address(this)) onlyLatestContract("rocketVault", msg.sender) {} /// @notice Accepts incoming ETH from megapool distributions for voter share into vault function depositVoterShare() override payable external { // Transfer incoming ETH directly to the vault RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); rocketVault.depositEther{value: msg.value}(); } /// @notice Returns the amount of ETH rewards waiting to be distributed function getEthBalance() override external view returns (uint256) { RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); return rocketVault.balanceOf("rocketRewardsPool"); } /// @notice Get the reward index function getRewardIndex() override public view returns (uint256) { return getUint(keccak256("rewards.snapshot.index")); } /// @notice Increment the reward index function _incrementRewardIndex() internal { addUint(keccak256("rewards.snapshot.index"), 1); } /// @notice Get how much RPL the Rewards Pool contract currently has assigned to it as a whole /// @return uint256 Returns rpl balance of rocket rewards contract function getRPLBalance() override public view returns (uint256) { // Get the vault contract instance RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Check contract RPL balance return rocketVault.balanceOfToken("rocketRewardsPool", IERC20(getContractAddress("rocketTokenRPL"))); } /// @notice Returns the total amount of RPL that needs to be distributed to claimers at the current block function getPendingRPLRewards() override public view returns (uint256) { RocketTokenRPLInterface rplContract = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); uint256 pendingInflation = rplContract.inflationCalculate(); // Any inflation that has accrued so far plus any amount that would be minted if we called it now return getRPLBalance() + pendingInflation; } /// @notice Returns the total amount of ETH in the smoothing pool ready to be distributed function getPendingETHRewards() override public view returns (uint256) { address rocketSmoothingPoolAddress = getContractAddress("rocketSmoothingPool"); return rocketSmoothingPoolAddress.balance; } /// @notice Returns the amount of pending voter share ETH ready to be distributed function getPendingVoterShare() override public view returns (uint256) { RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); return rocketVault.balanceOf("rocketRewardsPool"); } /// @notice Get the last set interval start time /// @return uint256 Last set start timestamp for a claim interval function getClaimIntervalTimeStart() override public view returns (uint256) { return getUint(keccak256("rewards.pool.claim.interval.time.start")); } /// @notice Get how many seconds in a claim interval /// @return uint256 Number of seconds in a claim interval function getClaimIntervalTime() override public view returns (uint256) { // Get from the DAO settings RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); return daoSettingsRewards.getRewardsClaimIntervalTime(); } /// @notice Compute intervals since last claim period /// @return uint256 Time intervals since last update function getClaimIntervalsPassed() override public view returns (uint256) { return (block.timestamp - getClaimIntervalTimeStart()) / getClaimIntervalTime(); } /// @notice Returns the block number that the given claim interval was executed at /// @param _interval The interval for which to grab the execution block of function getClaimIntervalExecutionBlock(uint256 _interval) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked("rewards.pool.interval.execution.block", _interval))); } /// @notice Returns the address of the contract which was used to execute this reward interval /// @param _interval The interval for which to grab the address of function getClaimIntervalExecutionAddress(uint256 _interval) override external view returns (address) { return getAddress(keccak256(abi.encodePacked("rewards.pool.interval.execution.address", _interval))); } /// @notice Get the percentage this contract can claim in this interval /// @return uint256 Rewards percentage this contract can claim in this interval function getClaimingContractPerc(string memory _claimingContract) override public view returns (uint256) { // Load contract RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); // Get the % amount allocated to this claim contract return daoSettingsRewards.getRewardsClaimerPerc(_claimingContract); } /// @notice Get an array of percentages that the given contracts can claim in this interval /// @return uint256[] Array of percentages in the order of the supplied contract names function getClaimingContractsPerc(string[] memory _claimingContracts) override external view returns (uint256[] memory) { // Load contract RocketDAOProtocolSettingsRewardsInterface daoSettingsRewards = RocketDAOProtocolSettingsRewardsInterface(getContractAddress("rocketDAOProtocolSettingsRewards")); // Get the % amount allocated to this claim contract uint256[] memory percentages = new uint256[](_claimingContracts.length); for (uint256 i = 0; i < _claimingContracts.length; ++i) { percentages[i] = daoSettingsRewards.getRewardsClaimerPerc(_claimingContracts[i]); } return percentages; } /// @notice Returns whether a trusted node has submitted for a given reward index function getTrustedNodeSubmitted(address _trustedNodeAddress, uint256 _rewardIndex) override external view returns (bool) { return getBool(keccak256(abi.encode("rewards.snapshot.submitted.node", _trustedNodeAddress, _rewardIndex))); } /// @notice Returns whether a trusted node has submitted a specific RewardSubmission function getSubmissionFromNodeExists(address _trustedNodeAddress, RewardSubmission calldata _submission) override external view returns (bool) { return getBool(keccak256(abi.encode("rewards.snapshot.submitted.node.key", _trustedNodeAddress, _submission))); } /// @notice Returns the number of trusted nodes who have agreed to the given submission function getSubmissionCount(RewardSubmission calldata _submission) override external view returns (uint256) { return getUint(keccak256(abi.encode("rewards.snapshot.submitted.count", _submission))); } /// @notice Submit a reward snapshot. Only accepts calls from trusted (oracle) nodes function submitRewardSnapshot(RewardSubmission calldata _submission) override external onlyLatestContract("rocketRewardsPool", address(this)) onlyTrustedNode(msg.sender) { // Get contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); // Check submission is currently enabled require(rocketDAOProtocolSettingsNetwork.getSubmitRewardsEnabled(), "Submitting rewards is currently disabled"); // Validate inputs uint256 rewardIndex = getRewardIndex(); require(_submission.rewardIndex <= rewardIndex, "Can only submit snapshot for periods up to next"); require(_submission.intervalsPassed > 0, "Invalid number of intervals passed"); require(_submission.nodeRPL.length == _submission.trustedNodeRPL.length && _submission.trustedNodeRPL.length == _submission.nodeETH.length, "Invalid array length"); // Calculate RPL reward total and validate { // Scope to prevent stack too deep uint256 totalRewardsRPL = _submission.treasuryRPL; for (uint256 i = 0; i < _submission.nodeRPL.length; ++i) { totalRewardsRPL = totalRewardsRPL + _submission.nodeRPL[i]; } for (uint256 i = 0; i < _submission.trustedNodeRPL.length; ++i) { totalRewardsRPL = totalRewardsRPL + _submission.trustedNodeRPL[i]; } require(totalRewardsRPL <= getPendingRPLRewards(), "Invalid RPL rewards"); } // Calculate ETH reward total and validate { // Scope to prevent stack too deep uint256 totalRewardsETH = _submission.treasuryETH + _submission.userETH; for (uint256 i = 0; i < _submission.nodeETH.length; ++i) { totalRewardsETH = totalRewardsETH + _submission.nodeETH[i]; } uint256 smoothingPoolBalance = getPendingETHRewards(); require(totalRewardsETH <= smoothingPoolBalance + getPendingVoterShare(), "Invalid ETH rewards"); require(_submission.smoothingPoolETH <= smoothingPoolBalance, "Invalid smoothing pool balance"); } // Store and increment vote uint256 submissionCount; { // Scope to prevent stack too deep // Check & update node submission status bytes32 nodeSubmissionKey = keccak256(abi.encode("rewards.snapshot.submitted.node.key", msg.sender, _submission)); require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encode("rewards.snapshot.submitted.node", msg.sender, _submission.rewardIndex)), true); } { // Scope to prevent stack too deep // Increment submission count bytes32 submissionCountKey = keccak256(abi.encode("rewards.snapshot.submitted.count", _submission)); submissionCount = getUint(submissionCountKey) + 1; setUint(submissionCountKey, submissionCount); } // Emit snapshot submitted event emit RewardSnapshotSubmitted(msg.sender, _submission.rewardIndex, _submission, block.timestamp); // Return if already executed if (_submission.rewardIndex != rewardIndex) { return; } // If consensus is reached, execute the snapshot RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); if (calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { _executeRewardSnapshot(_submission); } } /// @notice Executes reward snapshot if consensus threshold is reached function executeRewardSnapshot(RewardSubmission calldata _submission) override external onlyLatestContract("rocketRewardsPool", address(this)) { // Validate reward index of submission require(_submission.rewardIndex == getRewardIndex(), "Can only execute snapshot for next period"); // Get submission count bytes32 submissionCountKey = keccak256(abi.encode("rewards.snapshot.submitted.count", _submission)); uint256 submissionCount = getUint(submissionCountKey); // Confirm consensus and execute RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold(), "Consensus has not been reached"); _executeRewardSnapshot(_submission); } /// @notice Executes reward snapshot and sends assets to the relays for distribution to reward recipients function _executeRewardSnapshot(RewardSubmission calldata _submission) internal { // Get contract RocketTokenRPLInterface rplContract = RocketTokenRPLInterface(getContractAddress("rocketTokenRPL")); RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); // Execute inflation if required rplContract.inflationMintTokens(); // Increment the reward index and update the claim interval timestamp _incrementRewardIndex(); uint256 claimIntervalTimeStart = getClaimIntervalTimeStart(); uint256 claimIntervalTimeEnd = claimIntervalTimeStart + (getClaimIntervalTime() * _submission.intervalsPassed); // Emit reward snapshot event emit RewardSnapshot(_submission.rewardIndex, _submission, claimIntervalTimeStart, claimIntervalTimeEnd, block.timestamp); setUint(keccak256(abi.encodePacked("rewards.pool.interval.execution.block", _submission.rewardIndex)), block.number); setAddress(keccak256(abi.encodePacked("rewards.pool.interval.execution.address", _submission.rewardIndex)), address(this)); setUint(keccak256("rewards.pool.claim.interval.time.start"), claimIntervalTimeEnd); // Send out the treasury rewards if (_submission.treasuryRPL > 0) { rocketVault.transferToken("rocketClaimDAO", rplContract, _submission.treasuryRPL); } // Get the smoothing pool instance RocketSmoothingPoolInterface rocketSmoothingPool = RocketSmoothingPoolInterface(getContractAddress("rocketSmoothingPool")); // Withdraw ETH from the smoothing pool required for this interval if (_submission.smoothingPoolETH > 0) { rocketSmoothingPool.withdrawEther(address(this), _submission.smoothingPoolETH); } // Calculate total amount of ETH required for this reward interval uint256 totalETH = _submission.userETH + _submission.treasuryETH; for (uint i = 0; i < _submission.nodeETH.length; ++i) { totalETH += _submission.nodeETH[i]; } // Withdraw remaining ETH required from the vault uint256 vaultBalance = totalETH - _submission.smoothingPoolETH; if (vaultBalance > 0) { rocketVault.withdrawEther(vaultBalance); } // Send user share to rETH contract if (_submission.userETH > 0) { address rocketTokenRETHAddress = getContractAddress("rocketTokenRETH"); (bool result,) = rocketTokenRETHAddress.call{value: _submission.userETH}(""); require(result, "Failed to send user rewards"); } // Send pDAO share to treasury if (_submission.treasuryETH > 0) { address payable rocketClaimDAO = payable(getContractAddress("rocketClaimDAO")); (bool result,) = rocketClaimDAO.call{value: _submission.treasuryETH}(""); require(result, "Failed to send pDAO rewards"); } // Loop over each network and distribute rewards for (uint i = 0; i < _submission.nodeRPL.length; ++i) { // Quick out if no rewards for this network uint256 rewardsRPL = _submission.nodeRPL[i] + _submission.trustedNodeRPL[i]; uint256 rewardsETH = _submission.nodeETH[i]; if (rewardsRPL == 0 && rewardsETH == 0) { continue; } // Grab the relay address RocketRewardsRelayInterface relay; { // Scope to prevent stack too deep address networkRelayAddress; bytes32 networkRelayKey = keccak256(abi.encodePacked("rewards.relay.address", i)); networkRelayAddress = getAddress(networkRelayKey); // Validate network is valid require(networkRelayAddress != address(0), "Snapshot contains rewards for invalid network"); relay = RocketRewardsRelayInterface(networkRelayAddress); } // Transfer rewards if (rewardsRPL > 0) { // RPL rewards are withdrawn from the vault directly to the relay rocketVault.withdrawToken(address(relay), rplContract, rewardsRPL); } if (rewardsETH > 0) { // Send ETH rewards to the relay (bool result,) = address(relay).call{value: rewardsETH}(""); require(result, "Failed to send ETH rewards to relay"); } // Call into relay contract to handle distribution of rewards relay.relayRewards(_submission.rewardIndex, treeVersion, _submission.merkleRoot, rewardsRPL, rewardsETH); } } } ================================================ FILE: contracts/contract/rewards/RocketSmoothingPool.sol ================================================ pragma solidity 0.7.6; pragma abicoder v2; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/rewards/RocketSmoothingPoolInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; /* Receives priority fees and MEV via fee_recipient NOTE: This contract intentionally does not use RocketVault to store ETH because there is no way to account for ETH being added to this contract via fee_recipient. This also means if this contract is upgraded, the ETH must be manually transferred from this contract to the upgraded one. */ contract RocketSmoothingPool is RocketBase, RocketSmoothingPoolInterface { // Libs using SafeMath for uint256; // Events event EtherWithdrawn(string indexed by, address indexed to, uint256 amount, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version version = 1; } // Allow receiving ETH receive() payable external {} // Withdraws ETH to given address // Only accepts calls from Rocket Pool network contracts function withdrawEther(address _to, uint256 _amount) override external onlyLatestNetworkContract { // Valid amount? require(_amount > 0, "No valid amount of ETH given to withdraw"); // Get contract name string memory contractName = getContractName(msg.sender); // Send the ETH (bool result,) = _to.call{value: _amount}(""); require(result, "Failed to withdraw ETH"); // Emit ether withdrawn event emit EtherWithdrawn(contractName, _to, _amount, block.timestamp); } } ================================================ FILE: contracts/contract/token/RocketTokenRETH.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../util/ERC20.sol"; import "../RocketBase.sol"; import "../../interface/deposit/RocketDepositPoolInterface.sol"; import "../../interface/network/RocketNetworkBalancesInterface.sol"; import "../../interface/token/RocketTokenRETHInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol"; // rETH is a tokenised stake in the Rocket Pool network // rETH is backed by ETH (subject to liquidity) at a variable exchange rate contract RocketTokenRETH is RocketBase, ERC20, RocketTokenRETHInterface { // Libs using SafeMath for uint; // Events event EtherDeposited(address indexed from, uint256 amount, uint256 time); event TokensMinted(address indexed to, uint256 amount, uint256 ethAmount, uint256 time); event TokensBurned(address indexed from, uint256 amount, uint256 ethAmount, uint256 time); // Construct with our token details constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) ERC20("Rocket Pool ETH", "rETH") { // Version version = 1; } // Receive an ETH deposit from a minipool or generous individual receive() external payable { // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); } // Calculate the amount of ETH backing an amount of rETH function getEthValue(uint256 _rethAmount) override public view returns (uint256) { // Get network balances RocketNetworkBalancesInterface rocketNetworkBalances = RocketNetworkBalancesInterface(getContractAddress("rocketNetworkBalances")); uint256 totalEthBalance = rocketNetworkBalances.getTotalETHBalance(); uint256 rethSupply = rocketNetworkBalances.getTotalRETHSupply(); // Use 1:1 ratio if no rETH is minted if (rethSupply == 0) { return _rethAmount; } // Calculate and return return _rethAmount.mul(totalEthBalance).div(rethSupply); } // Calculate the amount of rETH backed by an amount of ETH function getRethValue(uint256 _ethAmount) override public view returns (uint256) { // Get network balances RocketNetworkBalancesInterface rocketNetworkBalances = RocketNetworkBalancesInterface(getContractAddress("rocketNetworkBalances")); uint256 totalEthBalance = rocketNetworkBalances.getTotalETHBalance(); uint256 rethSupply = rocketNetworkBalances.getTotalRETHSupply(); // Use 1:1 ratio if no rETH is minted if (rethSupply == 0) { return _ethAmount; } // Check network ETH balance require(totalEthBalance > 0, "Cannot calculate rETH token amount while total network balance is zero"); // Calculate and return return _ethAmount.mul(rethSupply).div(totalEthBalance); } // Get the current ETH : rETH exchange rate // Returns the amount of ETH backing 1 rETH function getExchangeRate() override external view returns (uint256) { return getEthValue(1 ether); } // Get the total amount of collateral available // Includes rETH contract balance & excess deposit pool balance function getTotalCollateral() override public view returns (uint256) { RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); return rocketDepositPool.getExcessBalance().add(address(this).balance); } // Get the current ETH collateral rate // Returns the portion of rETH backed by ETH in the contract as a fraction of 1 ether function getCollateralRate() override public view returns (uint256) { uint256 totalEthValue = getEthValue(totalSupply()); if (totalEthValue == 0) { return calcBase; } return calcBase.mul(address(this).balance).div(totalEthValue); } // Deposit excess ETH from deposit pool // Only accepts calls from the RocketDepositPool contract function depositExcess() override external payable onlyLatestContract("rocketDepositPool", msg.sender) { // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); } // Mint rETH // Only accepts calls from the RocketDepositPool contract function mint(uint256 _ethAmount, address _to) override external onlyLatestContract("rocketDepositPool", msg.sender) { // Get rETH amount uint256 rethAmount = getRethValue(_ethAmount); // Check rETH amount require(rethAmount > 0, "Invalid token mint amount"); // Update balance & supply _mint(_to, rethAmount); // Emit tokens minted event emit TokensMinted(_to, rethAmount, _ethAmount, block.timestamp); } // Burn rETH for ETH function burn(uint256 _rethAmount) override external { // Check rETH amount require(_rethAmount > 0, "Invalid token burn amount"); require(balanceOf(msg.sender) >= _rethAmount, "Insufficient rETH balance"); // Get ETH amount uint256 ethAmount = getEthValue(_rethAmount); // Get & check ETH balance uint256 ethBalance = getTotalCollateral(); require(ethBalance >= ethAmount, "Insufficient ETH balance for exchange"); // Update balance & supply _burn(msg.sender, _rethAmount); // Withdraw ETH from deposit pool if required withdrawDepositCollateral(ethAmount); // Transfer ETH to sender msg.sender.transfer(ethAmount); // Emit tokens burned event emit TokensBurned(msg.sender, _rethAmount, ethAmount, block.timestamp); } // Withdraw ETH from the deposit pool for collateral if required function withdrawDepositCollateral(uint256 _ethRequired) private { // Check rETH contract balance uint256 ethBalance = address(this).balance; if (ethBalance >= _ethRequired) { return; } // Withdraw RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance)); } // Sends any excess ETH from this contract to the deposit pool (as determined by target collateral rate) function depositExcessCollateral() external override { // Load contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); // Get collateral and target collateral rate uint256 collateralRate = getCollateralRate(); uint256 targetCollateralRate = rocketDAOProtocolSettingsNetwork.getTargetRethCollateralRate(); // Check if we are in excess if (collateralRate > targetCollateralRate) { // Calculate our target collateral in ETH uint256 targetCollateral = address(this).balance.mul(targetCollateralRate).div(collateralRate); // If we have excess if (address(this).balance > targetCollateral) { // Send that excess to deposit pool uint256 excessCollateral = address(this).balance.sub(targetCollateral); rocketDepositPool.recycleExcessCollateral{value: excessCollateral}(); } } } // This is called by the base ERC20 contract before all transfer, mint, and burns function _beforeTokenTransfer(address from, address, uint256) internal override { // Don't run check if this is a mint transaction if (from != address(0)) { // Check which block the user's last deposit was bytes32 key = keccak256(abi.encodePacked("user.deposit.block", from)); uint256 lastDepositBlock = getUint(key); if (lastDepositBlock > 0) { // Ensure enough blocks have passed uint256 depositDelay = getUint(keccak256(abi.encodePacked(keccak256("dao.protocol.setting.network"), "network.reth.deposit.delay"))); uint256 blocksPassed = block.number.sub(lastDepositBlock); require(blocksPassed > depositDelay, "Not enough time has passed since deposit"); // Clear the state as it's no longer necessary to check this until another deposit is made deleteUint(key); } } } } ================================================ FILE: contracts/contract/token/RocketTokenRPL.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsInflationInterface.sol"; import "../../interface/token/RocketTokenRPLInterface.sol"; import "../../interface/RocketVaultInterface.sol"; import "../util/ERC20Burnable.sol"; import "../util/SafeMath.sol"; // RPL Governance and utility token // Inlfationary with rate determined by DAO contract RocketTokenRPL is RocketBase, ERC20Burnable, RocketTokenRPLInterface { // Libs using SafeMath for uint; /**** Properties ***********/ // How many RPL tokens minted to date (18m from fixed supply) uint256 constant totalInitialSupply = 18000000000000000000000000; // The RPL inflation interval uint256 constant inflationInterval = 1 days; // How many RPL tokens have been swapped for new ones uint256 public totalSwappedRPL = 0; // Timestamp of last block inflation was calculated at uint256 private inflationCalcTime = 0; /**** Contracts ************/ // The address of our fixed supply RPL ERC20 token contract IERC20 rplFixedSupplyContract = IERC20(address(0)); /**** Events ***********/ event RPLInflationLog(address sender, uint256 value, uint256 inflationCalcTime); event RPLFixedSupplyBurn(address indexed from, uint256 amount, uint256 time); // Construct constructor(RocketStorageInterface _rocketStorageAddress, IERC20 _rocketTokenRPLFixedSupplyAddress) RocketBase(_rocketStorageAddress) ERC20("Rocket Pool Protocol", "RPL") { // Version version = 1; // Set the mainnet RPL fixed supply token address rplFixedSupplyContract = IERC20(_rocketTokenRPLFixedSupplyAddress); // Mint the 18m tokens that currently exist and allow them to be sent to people burning existing fixed supply RPL _mint(address(this), totalInitialSupply); } /** * Get the last time that inflation was calculated at * @return uint256 Last timestamp since inflation was calculated */ function getInflationCalcTime() override public view returns(uint256) { // Get the last time inflation was calculated if it has even started uint256 inflationStartTime = getInflationIntervalStartTime(); // If inflation has just begun but not been calculated previously, use the start block as the last calculated point if it has passed return inflationCalcTime == 0 && inflationStartTime < block.timestamp ? inflationStartTime : inflationCalcTime; } /** * How many seconds to calculate inflation at * @return uint256 how many seconds to calculate inflation at */ function getInflationIntervalTime() override external pure returns(uint256) { return inflationInterval; } /** * The current inflation rate per interval (eg 1000133680617113500 = 5% annual) * @return uint256 The current inflation rate per interval */ function getInflationIntervalRate() override public view returns(uint256) { // Inflation rate controlled by the DAO RocketDAOProtocolSettingsInflationInterface daoSettingsInflation = RocketDAOProtocolSettingsInflationInterface(getContractAddress("rocketDAOProtocolSettingsInflation")); return daoSettingsInflation.getInflationIntervalRate(); } /** * The current block to begin inflation at * @return uint256 The current block to begin inflation at */ function getInflationIntervalStartTime() override public view returns(uint256) { // Inflation rate start time controlled by the DAO RocketDAOProtocolSettingsInflationInterface daoSettingsInflation = RocketDAOProtocolSettingsInflationInterface(getContractAddress("rocketDAOProtocolSettingsInflation")); return daoSettingsInflation.getInflationIntervalStartTime(); } /** * The current rewards pool address that receives the inflation * @return address The rewards pool contract address */ function getInflationRewardsContractAddress() override external view returns(address) { // Inflation rate start block controlled by the DAO return getContractAddress("rocketRewardsPool"); } /** * Compute interval since last inflation update (on call) * @return uint256 Time intervals since last update */ function getInflationIntervalsPassed() override public view returns(uint256) { // The time that inflation was last calculated at uint256 inflationLastCalculatedTime = getInflationCalcTime(); return _getInflationIntervalsPassed(inflationLastCalculatedTime); } function _getInflationIntervalsPassed(uint256 _inflationLastCalcTime) private view returns(uint256) { // Calculate now if inflation has begun if(_inflationLastCalcTime > 0) { return (block.timestamp).sub(_inflationLastCalcTime).div(inflationInterval); }else{ return 0; } } /** * @dev Function to compute how many tokens should be minted * @return A uint256 specifying number of new tokens to mint */ function inflationCalculate() override external view returns (uint256) { uint256 intervalsSinceLastMint = getInflationIntervalsPassed(); return _inflationCalculate(intervalsSinceLastMint); } function _inflationCalculate(uint256 _intervalsSinceLastMint) private view returns (uint256) { // The inflation amount uint256 inflationTokenAmount = 0; // Only update if last interval has passed and inflation rate is > 0 if(_intervalsSinceLastMint > 0) { // Optimisation uint256 inflationRate = getInflationIntervalRate(); if(inflationRate > 0) { // Get the total supply now uint256 totalSupplyCurrent = totalSupply(); uint256 newTotalSupply = totalSupplyCurrent; // Compute inflation for total inflation intervals elapsed for (uint256 i = 0; i < _intervalsSinceLastMint; i++) { newTotalSupply = newTotalSupply.mul(inflationRate).div(10**18); } // Return inflation amount inflationTokenAmount = newTotalSupply.sub(totalSupplyCurrent); } } // Done return inflationTokenAmount; } /** * @dev Mint new tokens if enough time has elapsed since last mint * @return A uint256 specifying number of new tokens that were minted */ function inflationMintTokens() override external returns (uint256) { // Only run inflation process if at least 1 interval has passed (function returns 0 otherwise) uint256 inflationLastCalcTime = getInflationCalcTime(); uint256 intervalsSinceLastMint = _getInflationIntervalsPassed(inflationLastCalcTime); if (intervalsSinceLastMint == 0) { return 0; } // Address of the vault where to send tokens address rocketVaultAddress = getContractAddress("rocketVault"); require(rocketVaultAddress != address(0x0), "rocketVault address not set"); // Only mint if we have new tokens to mint since last interval and an address is set to receive them RocketVaultInterface rocketVaultContract = RocketVaultInterface(rocketVaultAddress); // Calculate the amount of tokens now based on inflation rate uint256 newTokens = _inflationCalculate(intervalsSinceLastMint); // Update last inflation calculation timestamp even if inflation rate is 0 inflationCalcTime = inflationLastCalcTime.add(inflationInterval.mul(intervalsSinceLastMint)); // Check if actually need to mint tokens (e.g. inflation rate > 0) if (newTokens > 0) { // Mint to itself, then allocate tokens for transfer to rewards contract, this will update balance & supply _mint(address(this), newTokens); // Initialise itself and allow from it's own balance (cant just do an allow as it could be any user calling this so they are msg.sender) IERC20 rplInflationContract = IERC20(address(this)); // Get the current allowance for Rocket Vault uint256 vaultAllowance = rplFixedSupplyContract.allowance(rocketVaultAddress, address(this)); // Now allow Rocket Vault to move those tokens, we also need to account of any other allowances for this token from other contracts in the same block require(rplInflationContract.approve(rocketVaultAddress, vaultAllowance.add(newTokens)), "Allowance for Rocket Vault could not be approved"); // Let vault know it can move these tokens to itself now and credit the balance to the RPL rewards pool contract rocketVaultContract.depositToken("rocketRewardsPool", IERC20(address(this)), newTokens); } // Log it emit RPLInflationLog(msg.sender, newTokens, inflationCalcTime); // return number minted return newTokens; } /** * @dev Swap current RPL fixed supply tokens for new RPL 1:1 to the same address from the user calling it * @param _amount The amount of RPL fixed supply tokens to swap */ function swapTokens(uint256 _amount) override external { // Valid amount? require(_amount > 0, "Please enter valid amount of RPL to swap"); // Send the tokens to this contract now and mint new ones for them require(rplFixedSupplyContract.transferFrom(msg.sender, address(this), _amount), "Token transfer from existing RPL contract was not successful"); // Transfer from the contracts RPL balance to the user require(this.transfer(msg.sender, _amount), "Token transfer from RPL inflation contract was not successful"); // Update the total swapped totalSwappedRPL = totalSwappedRPL.add(_amount); // Log it emit RPLFixedSupplyBurn(msg.sender, _amount, block.timestamp); } } ================================================ FILE: contracts/contract/token/temp/RocketTokenDummyRPL.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /// @title Dummy Rocket Pool Token (RPL) contract (do not deploy to mainnet) /// @author Jake Pospischil contract RocketTokenDummyRPL is ERC20, Ownable { /**** Properties ***********/ uint8 constant decimalPlaces = 18; uint256 constant public exponent = 10**uint256(decimalPlaces); uint256 constant public totalSupplyCap = 18.5 * (10**6) * exponent; // 18 Million tokens /**** Libs *****************/ using SafeMath for uint; /*** Events ****************/ event MintToken(address _minter, address _address, uint256 _value); /**** Methods ***********/ // Construct with our token details constructor(address _rocketStorageAddress) ERC20("Rocket Pool Dummy RPL", "DRPL") {} // @dev Mint the Rocket Pool Tokens (RPL) // @param _to The address that will receive the minted tokens. // @param _amount The amount of tokens to mint. // @return A boolean that indicates if the operation was successful. function mint(address _to, uint _amount) external onlyOwner returns (bool) { // Check token amount is positive require(_amount > 0); // Check we don't exceed the supply cap require(totalSupply().add(_amount) <= totalSupplyCap); // Mint tokens at address _mint(_to, _amount); // Fire mint token event emit MintToken(msg.sender, _to, _amount); // Return success flag return true; } /// @dev Returns the amount of tokens that can still be minted function getRemainingTokens() external view returns(uint256) { return totalSupplyCap.sub(totalSupply()); } } ================================================ FILE: contracts/contract/util/AddressQueueStorage.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "@openzeppelin/contracts/math/SafeMath.sol"; import "../RocketBase.sol"; import "../../interface/util/AddressQueueStorageInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // Address queue storage helper for RocketStorage data (ring buffer implementation) contract AddressQueueStorage is RocketBase, AddressQueueStorageInterface { // Libs using SafeMath for uint256; // Settings uint256 constant public capacity = 2 ** 255; // max uint256 / 2 // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } // The number of items in a queue function getLength(bytes32 _key) override public view returns (uint) { uint start = getUint(keccak256(abi.encodePacked(_key, ".start"))); uint end = getUint(keccak256(abi.encodePacked(_key, ".end"))); if (end < start) { end = end.add(capacity); } return end.sub(start); } // The item in a queue by index function getItem(bytes32 _key, uint _index) override external view returns (address) { uint index = getUint(keccak256(abi.encodePacked(_key, ".start"))).add(_index); if (index >= capacity) { index = index.sub(capacity); } return getAddress(keccak256(abi.encodePacked(_key, ".item", index))); } // The index of an item in a queue // Returns -1 if the value is not found function getIndexOf(bytes32 _key, address _value) override external view returns (int) { int index = int(getUint(keccak256(abi.encodePacked(_key, ".index", _value)))) - 1; if (index != -1) { index -= int(getUint(keccak256(abi.encodePacked(_key, ".start")))); if (index < 0) { index += int(capacity); } } return index; } // Add an item to the end of a queue // Requires that the queue is not at capacity // Requires that the item does not exist in the queue function enqueueItem(bytes32 _key, address _value) override external onlyLatestContract("addressQueueStorage", address(this)) onlyLatestNetworkContract { require(getLength(_key) < capacity.sub(1), "Queue is at capacity"); require(getUint(keccak256(abi.encodePacked(_key, ".index", _value))) == 0, "Item already exists in queue"); uint index = getUint(keccak256(abi.encodePacked(_key, ".end"))); setAddress(keccak256(abi.encodePacked(_key, ".item", index)), _value); setUint(keccak256(abi.encodePacked(_key, ".index", _value)), index.add(1)); index = index.add(1); if (index >= capacity) { index = index.sub(capacity); } setUint(keccak256(abi.encodePacked(_key, ".end")), index); } // Remove an item from the start of a queue and return it // Requires that the queue is not empty function dequeueItem(bytes32 _key) override external onlyLatestContract("addressQueueStorage", address(this)) onlyLatestNetworkContract returns (address) { require(getLength(_key) > 0, "Queue is empty"); uint start = getUint(keccak256(abi.encodePacked(_key, ".start"))); address item = getAddress(keccak256(abi.encodePacked(_key, ".item", start))); start = start.add(1); if (start >= capacity) { start = start.sub(capacity); } setUint(keccak256(abi.encodePacked(_key, ".index", item)), 0); setUint(keccak256(abi.encodePacked(_key, ".start")), start); return item; } // Remove an item from a queue // Swaps the item with the last item in the queue and truncates it; computationally cheap // Requires that the item exists in the queue function removeItem(bytes32 _key, address _value) override external onlyLatestContract("addressQueueStorage", address(this)) onlyLatestNetworkContract { uint index = getUint(keccak256(abi.encodePacked(_key, ".index", _value))); require(index-- > 0, "Item does not exist in queue"); uint lastIndex = getUint(keccak256(abi.encodePacked(_key, ".end"))); if (lastIndex == 0) lastIndex = capacity; lastIndex = lastIndex.sub(1); if (index != lastIndex) { address lastItem = getAddress(keccak256(abi.encodePacked(_key, ".item", lastIndex))); setAddress(keccak256(abi.encodePacked(_key, ".item", index)), lastItem); setUint(keccak256(abi.encodePacked(_key, ".index", lastItem)), index.add(1)); } setUint(keccak256(abi.encodePacked(_key, ".index", _value)), 0); setUint(keccak256(abi.encodePacked(_key, ".end")), lastIndex); } } ================================================ FILE: contracts/contract/util/AddressSetStorage.sol ================================================ pragma solidity 0.7.6; // SPDX-License-Identifier: GPL-3.0-only import "../RocketBase.sol"; import "../../interface/util/AddressSetStorageInterface.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; // Address set storage helper for RocketStorage data (contains unique items; has reverse index lookups) contract AddressSetStorage is RocketBase, AddressSetStorageInterface { using SafeMath for uint; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } // The number of items in a set function getCount(bytes32 _key) override external view returns (uint) { return getUint(keccak256(abi.encodePacked(_key, ".count"))); } // The item in a set by index function getItem(bytes32 _key, uint _index) override external view returns (address) { return getAddress(keccak256(abi.encodePacked(_key, ".item", _index))); } // The index of an item in a set // Returns -1 if the value is not found function getIndexOf(bytes32 _key, address _value) override external view returns (int) { return int(getUint(keccak256(abi.encodePacked(_key, ".index", _value)))) - 1; } // Add an item to a set // Requires that the item does not exist in the set function addItem(bytes32 _key, address _value) override external onlyLatestContract("addressSetStorage", address(this)) onlyLatestNetworkContract { require(getUint(keccak256(abi.encodePacked(_key, ".index", _value))) == 0, "Item already exists in set"); uint count = getUint(keccak256(abi.encodePacked(_key, ".count"))); setAddress(keccak256(abi.encodePacked(_key, ".item", count)), _value); setUint(keccak256(abi.encodePacked(_key, ".index", _value)), count.add(1)); setUint(keccak256(abi.encodePacked(_key, ".count")), count.add(1)); } // Remove an item from a set // Swaps the item with the last item in the set and truncates it; computationally cheap // Requires that the item exists in the set function removeItem(bytes32 _key, address _value) override external onlyLatestContract("addressSetStorage", address(this)) onlyLatestNetworkContract { uint256 index = getUint(keccak256(abi.encodePacked(_key, ".index", _value))); require(index-- > 0, "Item does not exist in set"); uint count = getUint(keccak256(abi.encodePacked(_key, ".count"))); if (index < count.sub(1)) { address lastItem = getAddress(keccak256(abi.encodePacked(_key, ".item", count.sub(1)))); setAddress(keccak256(abi.encodePacked(_key, ".item", index)), lastItem); setUint(keccak256(abi.encodePacked(_key, ".index", lastItem)), index.add(1)); } setUint(keccak256(abi.encodePacked(_key, ".index", _value)), 0); setUint(keccak256(abi.encodePacked(_key, ".count")), count.sub(1)); } } ================================================ FILE: contracts/contract/util/BeaconStateVerifier.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {RocketBase} from "../RocketBase.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {SSZ} from "./SSZ.sol"; import {BeaconStateVerifierInterface, ValidatorProof, Validator, WithdrawalProof, SlotProof, Withdrawal} from "../../interface/util/BeaconStateVerifierInterface.sol"; contract BeaconStateVerifier is RocketBase, BeaconStateVerifierInterface { // Immutables uint256 internal immutable slotsPerHistoricalRoot; uint256 internal immutable historicalSummaryOffset; uint64 internal immutable slotPhase0; uint64 internal immutable slotAltair; uint64 internal immutable slotBellatrix; uint64 internal immutable slotCapella; uint64 internal immutable slotDeneb; uint64 internal immutable slotElectra; address internal immutable beaconRoots; // 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02 bytes32 internal immutable genesisWitness; // Enums enum Fork { PHASE_0, ALTAIR, BELLATRIX, CAPELLA, DENEB, ELECTRA } // Construct constructor(RocketStorageInterface _rocketStorageAddress, uint256 _slotsPerHistoricalRoot, uint64[5] memory _forkSlots, address _beaconRoots, uint256 _genesisTime, bytes32 _genesisValidatorRoot) RocketBase(_rocketStorageAddress) { version = 1; slotsPerHistoricalRoot = _slotsPerHistoricalRoot; beaconRoots = _beaconRoots; // Set fork slots slotPhase0 = 0; slotAltair = _forkSlots[0]; slotBellatrix = _forkSlots[1]; slotCapella = _forkSlots[2]; slotDeneb = _forkSlots[3]; slotElectra = _forkSlots[4]; // Historical summaries started being appended from Capella onwards, depending on the chain we might need an offset historicalSummaryOffset = slotCapella / slotsPerHistoricalRoot; // Compute the genesis_time/genesis_validator_root witness to protect slot proofs from changes to beacon state container genesisWitness = SSZ.efficientSha256(SSZ.toLittleEndian(_genesisTime), _genesisValidatorRoot); } /// @notice Verifies a proof about a validator on the beacon chain /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _slot Slot number that the proof was generated for /// @param _proof Proof of the validator function verifyValidator(uint64 _slotTimestamp, uint64 _slot, ValidatorProof calldata _proof) override external view returns(bool) { // Only support post-electra state proofs require(_slot >= slotElectra, "Invalid proof"); // Construct gindex SSZ.Path memory path = _pathBeaconBlockHeaderToStateRoot(); path = SSZ.concat(path, _pathBeaconStateToValidator(_proof.validatorIndex)); // Restore the block root for the supplied slot require(SSZ.length(path) == _proof.witnesses.length, "Invalid witness length"); bytes32 computedRoot = SSZ.restoreMerkleRoot(_merkleiseValidator(_proof.validator), SSZ.toIndex(path), _proof.witnesses); // Retrieve and compare the root with what we determined it should be from the given proof bytes32 root = _getParentBlockRoot(_slotTimestamp); return computedRoot == root; } /// @notice Verifies a proof about the existence of a withdrawal on the beacon chain /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _slot Slot number that the proof was generated for /// @param _proof Proof of the withdrawal function verifyWithdrawal(uint64 _slotTimestamp, uint64 _slot, WithdrawalProof calldata _proof) override external view returns(bool) { // Only support post-electra state proofs require(_slot >= slotElectra, "Invalid proof"); require(_proof.withdrawalSlot >= slotElectra, "Invalid proof"); // Construct gindex SSZ.Path memory path = _pathBeaconBlockHeaderToStateRoot(); path = SSZ.concat(path, _pathBeaconStateToPastBlockRoot(_slot, _proof.withdrawalSlot)); path = SSZ.concat(path, _pathBlockToWithdrawal(_proof.withdrawalNum)); // Merkleise the withdrawal struct bytes32 leaf = _merkleiseWithdrawal(_proof.withdrawal); // Restore the block root for the supplied slot require(SSZ.length(path) == _proof.witnesses.length, "Invalid witness length"); bytes32 computedRoot = SSZ.restoreMerkleRoot(leaf, SSZ.toIndex(path), _proof.witnesses); // Retrieve and compare the root with what we determined it should be from the given proof bytes32 root = _getParentBlockRoot(_slotTimestamp); return computedRoot == root; } /// @notice Verifies a proof about the slot /// @param _slotTimestamp Timestamp of the slot containing the parent block hash of the slot used for proofs /// @param _proof Proof of the slot value function verifySlot(uint64 _slotTimestamp, SlotProof calldata _proof) override external view returns(bool) { // Only support post-electra state proofs require(_proof.slot >= slotElectra, "Invalid proof"); /** * genesisWitness represents the merkleised root of genesis_time ++ genesis_validators_root * By checking it against a known-value for the chain we are on, we are protecting from a future hard fork * which modifies the gindex of `slot` which would allow someone to prove an invalid `slot` value. * * genesisWitness (witness[1]) ... * / \ / \ * genesis_time genesis_validators_root slot fork (witness[0]) ... */ require(_proof.witnesses[1] == genesisWitness, "Invalid genesis witness"); // Retrieve the parent block hash bytes32 root = _getParentBlockRoot(_slotTimestamp); // Construct gindex SSZ.Path memory path = _pathBeaconBlockHeaderToStateRoot(); path = SSZ.concat(path, _pathBeaconStateToSlot()); // Merkleise the slot number bytes32 leaf = SSZ.toLittleEndian(uint256(_proof.slot)); // Restore the block root for the supplied slot require(SSZ.length(path) == _proof.witnesses.length, "Invalid witness length"); bytes32 computedRoot = SSZ.restoreMerkleRoot(leaf, SSZ.toIndex(path), _proof.witnesses); // Retrieve and compare the root with what we determined it should be from the given proof return computedRoot == root; } /// @dev Gets the parent block root for a given slot /// @param _slotTimestamp Timestamp of the slot containing the parent block hash function _getParentBlockRoot(uint64 _slotTimestamp) internal view returns (bytes32) { (bool success, bytes memory result) = beaconRoots.staticcall(abi.encode(_slotTimestamp)); if (success && result.length > 0) { return abi.decode(result, (bytes32)); } // Fail revert("Block root is not available"); } /// @dev Returns whether the target slot is older than SLOTS_PER_HISTORICAL_ROOT indicating a proof must be for an older slot function _isHistoricalProof(uint64 _proofSlot, uint64 _targetSlot) internal view returns (bool) { require(_proofSlot > _targetSlot, "Invalid slot for proof"); return _targetSlot + slotsPerHistoricalRoot < _proofSlot; } /// @dev Returns the SSZ merkle root of a given withdrawal container function _merkleiseWithdrawal(Withdrawal calldata _withdrawal) internal view returns (bytes32) { bytes32 left = SSZ.efficientSha256(SSZ.toLittleEndian(_withdrawal.index), SSZ.toLittleEndian(_withdrawal.validatorIndex)); bytes32 right = SSZ.efficientSha256(_withdrawal.withdrawalCredentials, SSZ.toLittleEndian(_withdrawal.amountInGwei)); return SSZ.efficientSha256(left, right); } /// @dev Returns the SSZ merkle root of a given validator function _merkleiseValidator(Validator calldata _validator) internal view returns (bytes32) { bytes32 a = SSZ.efficientSha256(SSZ.merkleisePubkey(_validator.pubkey), _validator.withdrawalCredentials); bytes32 b = SSZ.efficientSha256(SSZ.toLittleEndian(_validator.effectiveBalance), SSZ.toLittleEndian(_validator.slashed ? 1 : 0)); bytes32 c = SSZ.efficientSha256(SSZ.toLittleEndian(uint256(_validator.activationEligibilityEpoch)), SSZ.toLittleEndian(uint256(_validator.activationEpoch))); bytes32 d = SSZ.efficientSha256(SSZ.toLittleEndian(uint256(_validator.exitEpoch)), SSZ.toLittleEndian(uint256(_validator.withdrawableEpoch))); a = SSZ.efficientSha256(a, b); b = SSZ.efficientSha256(c, d); return SSZ.efficientSha256(a,b); } /// @dev Returns the fork at a given slot function _slotToFork(uint64 _slot) internal view returns (Fork) { if (_slot >= slotElectra) return Fork.ELECTRA; if (_slot >= slotDeneb) return Fork.DENEB; if (_slot >= slotCapella) return Fork.CAPELLA; if (_slot >= slotBellatrix) return Fork.BELLATRIX; if (_slot >= slotAltair) return Fork.ALTAIR; return Fork.PHASE_0; } /// @dev Returns a partial gindex from a BeaconBlockHeader -> state_root function _pathBeaconBlockHeaderToStateRoot() internal view returns (SSZ.Path memory) { SSZ.Path memory path = SSZ.from(3, 3); // 0b011 (BeaconBlockHeader -> state_root) return path; } /// @dev Returns a partial gindex from a BeaconState -> validators[n] function _pathBeaconStateToValidator(uint40 _validatorIndex) internal view returns (SSZ.Path memory) { SSZ.Path memory path = SSZ.from(11, 6); // 0b001011 (BeaconState -> validators) path = SSZ.concat(path, SSZ.intoList(_validatorIndex, 40)); // validators -> validators[n] return path; } /// @dev Returns a partial gindex from a BeaconState -> slot function _pathBeaconStateToSlot() internal view returns (SSZ.Path memory) { SSZ.Path memory path = SSZ.from(2, 6); // 0b000010 (BeaconState -> slot) return path; } /// @dev Returns a partial gindex from BeaconState -> block_roots[n] (via historical_summaries if required) function _pathBeaconStateToPastBlockRoot(uint64 _slot, uint64 _pastSlot) internal view returns (SSZ.Path memory) { bool isHistorical = _isHistoricalProof(_slot, _pastSlot); SSZ.Path memory path; if (isHistorical) { path = SSZ.concat(path, SSZ.from(27, 6)); // 0b001011 (BeaconState -> historical_summaries) path = SSZ.concat(path, SSZ.intoList(uint248(uint256(_pastSlot) / slotsPerHistoricalRoot - historicalSummaryOffset), 24)); // historical_summaries -> historical_summaries[n] path = SSZ.concat(path, SSZ.from(0, 1)); // 0b0 (HistoricalSummary -> block_summary_root) } else { path = SSZ.concat(path, SSZ.from(5, 6)); // 0b000101 (BeaconState -> block_roots) } path = SSZ.concat(path, SSZ.intoVector(uint248(_pastSlot % slotsPerHistoricalRoot), 13)); // block_roots -> block_roots[n] return path; } /// @dev Returns a partial gindex from BeaconBlockHeader -> withdrwals[n] function _pathBlockToWithdrawal(uint16 _withdrawalNum) internal view returns (SSZ.Path memory) { SSZ.Path memory path = SSZ.from(4, 3); // 0b100 (BeaconBlockHeader -> body_root) path = SSZ.concat(path, SSZ.from(9, 4)); // 0b1001 (BeaconBlockBody -> execution_payload) path = SSZ.concat(path, SSZ.from(14, 5)); // 0b01110 (ExecutionPayload -> withdrawals) path = SSZ.concat(path, SSZ.intoList(_withdrawalNum, 4)); // withdrawals -> withdrawals[n] return path; } } ================================================ FILE: contracts/contract/util/Context.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity >0.5.0 <0.9.0; /* * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with GSN meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address payable) { return payable(msg.sender); } function _msgData() internal view virtual returns (bytes memory) { this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 return msg.data; } } ================================================ FILE: contracts/contract/util/ERC20.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity >0.5.0 <0.9.0; import "./SafeMath.sol"; import "./Context.sol"; import "../../interface/util/IERC20.sol"; /** * @dev Implementation of the {IERC20} interface. * * This implementation is agnostic to the way tokens are created. This means * that a supply mechanism has to be added in a derived contract using {_mint}. * For a generic mechanism see {ERC20PresetMinterPauser}. * * TIP: For a detailed writeup see our guide * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How * to implement supply mechanisms]. * * We have followed general OpenZeppelin guidelines: functions revert instead * of returning `false` on failure. This behavior is nonetheless conventional * and does not conflict with the expectations of ERC20 applications. * * Additionally, an {Approval} event is emitted on calls to {transferFrom}. * This allows applications to reconstruct the allowance for all accounts just * by listening to said events. Other implementations of the EIP may not emit * these events, as it isn't required by the specification. * * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ contract ERC20 is Context, IERC20 { using SafeMath for uint256; mapping (address => uint256) private _balances; mapping (address => mapping (address => uint256)) private _allowances; uint256 private _totalSupply; string private _name; string private _symbol; uint8 private _decimals; /** * @dev Sets the values for {name} and {symbol}, initializes {decimals} with * a default value of 18. * * To select a different value for {decimals}, use {_setupDecimals}. * * All three of these values are immutable: they can only be set once during * construction. */ constructor (string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; _decimals = 18; } /** * @dev Returns the name of the token. */ function name() public view virtual returns (string memory) { return _name; } /** * @dev Returns the symbol of the token, usually a shorter version of the * name. */ function symbol() public view virtual returns (string memory) { return _symbol; } /** * @dev Returns the number of decimals used to get its user representation. * For example, if `decimals` equals `2`, a balance of `505` tokens should * be displayed to a user as `5,05` (`505 / 10 ** 2`). * * Tokens usually opt for a value of 18, imitating the relationship between * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is * called. * * NOTE: This information is only used for _display_ purposes: it in * no way affects any of the arithmetic of the contract, including * {IERC20-balanceOf} and {IERC20-transfer}. */ function decimals() public view virtual returns (uint8) { return _decimals; } /** * @dev See {IERC20-totalSupply}. */ function totalSupply() public view virtual override returns (uint256) { return _totalSupply; } /** * @dev See {IERC20-balanceOf}. */ function balanceOf(address account) public view virtual override returns (uint256) { return _balances[account]; } /** * @dev See {IERC20-transfer}. * * Requirements: * * - `recipient` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ function transfer(address recipient, uint256 amount) public virtual override returns (bool) { _transfer(_msgSender(), recipient, amount); return true; } /** * @dev See {IERC20-allowance}. */ function allowance(address owner, address spender) public view virtual override returns (uint256) { return _allowances[owner][spender]; } /** * @dev See {IERC20-approve}. * * Requirements: * * - `spender` cannot be the zero address. */ function approve(address spender, uint256 amount) public virtual override returns (bool) { _approve(_msgSender(), spender, amount); return true; } /** * @dev See {IERC20-transferFrom}. * * Emits an {Approval} event indicating the updated allowance. This is not * required by the EIP. See the note at the beginning of {ERC20}. * * Requirements: * * - `sender` and `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. * - the caller must have allowance for ``sender``'s tokens of at least * `amount`. */ function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { _transfer(sender, recipient, amount); _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); return true; } /** * @dev Atomically increases the allowance granted to `spender` by the caller. * * This is an alternative to {approve} that can be used as a mitigation for * problems described in {IERC20-approve}. * * Emits an {Approval} event indicating the updated allowance. * * Requirements: * * - `spender` cannot be the zero address. */ function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); return true; } /** * @dev Atomically decreases the allowance granted to `spender` by the caller. * * This is an alternative to {approve} that can be used as a mitigation for * problems described in {IERC20-approve}. * * Emits an {Approval} event indicating the updated allowance. * * Requirements: * * - `spender` cannot be the zero address. * - `spender` must have allowance for the caller of at least * `subtractedValue`. */ function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); return true; } /** * @dev Moves tokens `amount` from `sender` to `recipient`. * * This is internal function is equivalent to {transfer}, and can be used to * e.g. implement automatic token fees, slashing mechanisms, etc. * * Emits a {Transfer} event. * * Requirements: * * - `sender` cannot be the zero address. * - `recipient` cannot be the zero address. * - `sender` must have a balance of at least `amount`. */ function _transfer(address sender, address recipient, uint256 amount) internal virtual { require(sender != address(0), "ERC20: transfer from the zero address"); require(recipient != address(0), "ERC20: transfer to the zero address"); _beforeTokenTransfer(sender, recipient, amount); _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); _balances[recipient] = _balances[recipient].add(amount); emit Transfer(sender, recipient, amount); } /** @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. * * Emits a {Transfer} event with `from` set to the zero address. * * Requirements: * * - `to` cannot be the zero address. */ function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); _beforeTokenTransfer(address(0), account, amount); _totalSupply = _totalSupply.add(amount); _balances[account] = _balances[account].add(amount); emit Transfer(address(0), account, amount); } /** * @dev Destroys `amount` tokens from `account`, reducing the * total supply. * * Emits a {Transfer} event with `to` set to the zero address. * * Requirements: * * - `account` cannot be the zero address. * - `account` must have at least `amount` tokens. */ function _burn(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: burn from the zero address"); _beforeTokenTransfer(account, address(0), amount); _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); _totalSupply = _totalSupply.sub(amount); emit Transfer(account, address(0), amount); } /** * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. * * This internal function is equivalent to `approve`, and can be used to * e.g. set automatic allowances for certain subsystems, etc. * * Emits an {Approval} event. * * Requirements: * * - `owner` cannot be the zero address. * - `spender` cannot be the zero address. */ function _approve(address owner, address spender, uint256 amount) internal virtual { require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); _allowances[owner][spender] = amount; emit Approval(owner, spender, amount); } /** * @dev Sets {decimals} to a value other than the default one of 18. * * WARNING: This function should only be called from the constructor. Most * applications that interact with token contracts will not expect * {decimals} to ever change, and may work incorrectly if it does. */ function _setupDecimals(uint8 decimals_) internal virtual { _decimals = decimals_; } /** * @dev Hook that is called before any transfer of tokens. This includes * minting and burning. * * Calling conditions: * * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens * will be to transferred to `to`. * - when `from` is zero, `amount` tokens will be minted for `to`. * - when `to` is zero, `amount` of ``from``'s tokens will be burned. * - `from` and `to` are never both zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } } ================================================ FILE: contracts/contract/util/ERC20Burnable.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity >0.5.0 <0.9.0; import "./ERC20.sol"; import "./Context.sol"; /** * @dev Extension of {ERC20} that allows token holders to destroy both their own * tokens and those that they have an allowance for, in a way that can be * recognized off-chain (via event analysis). */ abstract contract ERC20Burnable is Context, ERC20 { using SafeMath for uint256; /** * @dev Destroys `amount` tokens from the caller. * * See {ERC20-_burn}. */ function burn(uint256 amount) public virtual { _burn(_msgSender(), amount); } /** * @dev Destroys `amount` tokens from `account`, deducting from the caller's * allowance. * * See {ERC20-_burn} and {ERC20-allowance}. * * Requirements: * * - the caller must have allowance for ``accounts``'s tokens of at least * `amount`. */ function burnFrom(address account, uint256 amount) public virtual { uint256 decreasedAllowance = allowance(account, _msgSender()).sub(amount, "ERC20: burn amount exceeds allowance"); _approve(account, _msgSender(), decreasedAllowance); _burn(account, amount); } } ================================================ FILE: contracts/contract/util/LinkedListStorage.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; pragma abicoder v2; import {LinkedListStorageInterface} from "../../interface/util/LinkedListStorageInterface.sol"; import {RocketBase} from "../RocketBase.sol"; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; /// @notice A linked list storage helper for the deposit requests queue data contract LinkedListStorage is RocketBase, LinkedListStorageInterface { // Constants for packing queue metadata into a single uint256 uint256 constant internal startOffset = 256 - 64; uint256 constant internal endOffset = 256 - 128; uint256 constant internal lengthOffset = 256 - 192; // Constants for packing a deposit item (struct) into a single uint256 uint256 constant internal receiverOffset = 256 - 160; uint256 constant internal indexOffset = 256 - 160 - 32; uint256 constant internal suppliedOffset = 256 - 160 - 32 - 32; uint64 constant internal ones64Bits = 0xFFFFFFFFFFFFFFFF; // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { version = 1; } /// @notice The number of items in the queue /// @param _namespace defines the queue to be used function getLength(bytes32 _namespace) override public view returns (uint256) { return uint64(getUint(keccak256(abi.encodePacked(_namespace, ".data"))) >> lengthOffset); } /// @notice The item in a queue by index /// @param _namespace defines the queue to be used /// @param _index the item index function getItem(bytes32 _namespace, uint256 _index) override external view returns (DepositQueueValue memory) { uint256 packedValue = getUint(keccak256(abi.encodePacked(_namespace, ".item", _index))); return _unpackItem(packedValue); } /// @notice The index of an item in a queue. Returns 0 if the value is not found /// @param _namespace defines the queue to be used /// @param _key the deposit queue value function getIndexOf(bytes32 _namespace, DepositQueueKey memory _key) override external view returns (uint256) { return getUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId))); } /// @notice Returns the index of the item at the head of the list /// @param _namespace defines the queue to be used function getHeadIndex(bytes32 _namespace) override external view returns (uint256) { uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); return uint64(data >> startOffset); } /// @notice Finds an item index in a queue and returns the previous item /// @param _namespace defines the queue to be used /// @param _value the deposit queue value function getPreviousItem(bytes32 _namespace, DepositQueueValue memory _value) external view returns (DepositQueueValue memory previousItem) { uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId))); if (index > 0) { uint256 previousIndex = getUint(keccak256(abi.encodePacked(_namespace, ".prev", index))); previousItem = _unpackItem(getUint(keccak256(abi.encodePacked(_namespace, ".item", previousIndex)))); } } /// @notice Finds an item index in a queue and returns the next item /// @param _namespace defines the queue to be used /// @param _value the deposit queue value function getNextItem(bytes32 _namespace, DepositQueueValue memory _value) external view returns (DepositQueueValue memory nextItem) { uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId))); if (index > 0) { uint256 nextIndex = getUint(keccak256(abi.encodePacked(_namespace, ".next", index))); nextItem = _unpackItem(getUint(keccak256(abi.encodePacked(_namespace, ".item", nextIndex)))); } } /// @notice Add an item to the end of the list. Requires that the item does not exist in the list /// @param _namespace defines the queue to be used /// @param _item the deposit queue item to be added function enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) virtual override external onlyLatestContract("linkedListStorage", address(this)) onlyLatestNetworkContract { _enqueueItem(_namespace, _item); } /// @notice Internal function created to allow testing enqueueItem /// @param _namespace defines the queue to be used /// @param _item the deposit queue value function _enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) internal { require(getUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId))) == 0, "Item already exists in queue"); uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); uint256 endIndex = uint64(data >> endOffset); uint256 newIndex = endIndex + 1; if (endIndex > 0) { setUint(keccak256(abi.encodePacked(_namespace, ".next", endIndex)), newIndex); setUint(keccak256(abi.encodePacked(_namespace, ".prev", newIndex)), endIndex); } else { // clear the 64 bits used to stored the 'start' pointer data &= ~(uint256(ones64Bits) << startOffset); data |= newIndex << startOffset; } setUint(keccak256(abi.encodePacked(_namespace, ".item", newIndex)), _packItem(_item)); setUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId)), newIndex); // clear the 64 bits used to stored the 'end' pointer data &= ~(uint256(ones64Bits) << endOffset); data |= newIndex << endOffset; // Update the length of the queue uint256 currentLength = uint64(data >> lengthOffset); // clear the 64 bits used to stored the 'length' information data &= ~(uint256(ones64Bits) << lengthOffset); data |= (currentLength + 1) << lengthOffset; setUint(keccak256(abi.encodePacked(_namespace, ".data")), data); } /// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty /// @param _namespace defines the queue to be used function dequeueItem(bytes32 _namespace) public virtual override onlyLatestContract("linkedListStorage", address(this)) onlyLatestNetworkContract returns (DepositQueueValue memory item) { return _dequeueItem(_namespace); } /// @notice Returns the item from the start of the queue without removing it function peekItem(bytes32 _namespace) public virtual override view returns (DepositQueueValue memory item) { uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); uint256 length = uint64(data >> lengthOffset); require(length > 0, "Queue can't be empty"); uint256 start = uint64(data >> startOffset); uint256 packedItem = getUint(keccak256(abi.encodePacked(_namespace, ".item", start))); item = _unpackItem(packedItem); } /// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty /// @param _namespace defines the queue to be used function _dequeueItem(bytes32 _namespace) internal returns (DepositQueueValue memory item) { uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); uint256 length = uint64(data >> lengthOffset); require(length > 0, "Queue can't be empty"); uint256 start = uint64(data >> startOffset); uint256 packedItem = getUint(keccak256(abi.encodePacked(_namespace, ".item", start))); item = _unpackItem(packedItem); uint256 nextItem = getUint(keccak256(abi.encodePacked(_namespace, ".next", start))); // clear the 64 bits used to stored the 'start' pointer data &= ~(uint256(ones64Bits) << startOffset); data |= nextItem << startOffset; deleteUint(keccak256(abi.encodePacked(_namespace, ".index", item.receiver, item.validatorId))); if (nextItem > 0) { deleteUint(keccak256(abi.encodePacked(_namespace, ".prev", nextItem))); } else { // zero the 64 bits storing the 'end' pointer data &= ~(uint256(ones64Bits) << endOffset); } // Update the length of the queue // clear the 64 bits used to stored the 'length' information data &= ~(uint256(ones64Bits) << lengthOffset); data |= (length - 1) << lengthOffset; setUint(keccak256(abi.encodePacked(_namespace, ".data")), data); // Clean up state deleteUint(keccak256(abi.encodePacked(_namespace, ".next", start))); deleteUint(keccak256(abi.encodePacked(_namespace, ".prev", start))); deleteUint(keccak256(abi.encodePacked(_namespace, ".item", start))); return item; } /// @notice Removes an item from a queue. Requires that the item exists in the queue /// @param _namespace defines the queue to be used /// @param _key to be removed from the queue function removeItem(bytes32 _namespace, DepositQueueKey memory _key) public virtual override onlyLatestContract("linkedListStorage", address(this)) onlyLatestNetworkContract { _removeItem(_namespace, _key); } /// @notice Internal function to remove an item from a queue. Requires that the item exists in the queue /// @param _namespace defines the queue to be used /// @param _key to be removed from the queue function _removeItem(bytes32 _namespace, DepositQueueKey memory _key) internal { uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId))); uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); require(index > 0, "Item does not exist in queue"); uint256 prevIndex = getUint(keccak256(abi.encodePacked(_namespace, ".prev", index))); uint256 nextIndex = getUint(keccak256(abi.encodePacked(_namespace, ".next", index))); if (prevIndex > 0) { // Not the first item setUint(keccak256(abi.encodePacked(_namespace, ".next", prevIndex)), nextIndex); } else { // First item // clear the 64 bits used to stored the 'start' pointer data &= ~(uint256(ones64Bits) << startOffset); data |= nextIndex << startOffset; deleteUint(keccak256(abi.encodePacked(_namespace, ".prev", nextIndex))); } if (nextIndex > 0) { // Not the last item setUint(keccak256(abi.encodePacked(_namespace, ".prev", nextIndex)), prevIndex); } else { // Last item // clear the 64 bits used to stored the 'end' pointer data &= ~(uint256(ones64Bits) << endOffset); data |= prevIndex << endOffset; } // Clean up state deleteUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId))); deleteUint(keccak256(abi.encodePacked(_namespace, ".next", index))); deleteUint(keccak256(abi.encodePacked(_namespace, ".prev", index))); deleteUint(keccak256(abi.encodePacked(_namespace, ".item", index))); // Update the length of the queue uint256 currentLength = uint64(data >> lengthOffset); // clear the 64 bits used to stored the 'length' information data &= ~(uint256(ones64Bits) << lengthOffset); data |= (currentLength - 1) << lengthOffset; setUint(keccak256(abi.encodePacked(_namespace, ".data")), data); } /// @notice packs a deposit queue value into a single uint256 /// @param _item the deposit queue item to be packed function _packItem(DepositQueueValue memory _item) internal pure returns (uint256 packed) { packed |= uint256(uint160(_item.receiver)) << receiverOffset; packed |= uint256(_item.validatorId) << indexOffset; packed |= uint256(_item.suppliedValue) << suppliedOffset; packed |= uint256(_item.requestedValue); } /// @notice unpacks an uint256 value into a deposit queue struct /// @param _packedValue the packed deposit queue value function _unpackItem(uint256 _packedValue) internal pure returns (DepositQueueValue memory item) { item.receiver = address(uint160(_packedValue >> receiverOffset)); item.validatorId = uint32(_packedValue >> indexOffset); item.suppliedValue = uint32(_packedValue >> suppliedOffset); item.requestedValue = uint32(_packedValue); } /// @notice Returns the supplied number of entries starting at the supplied index /// @param _namespace The namespace of the linked list to scan /// @param _start The index to start from, or 0 to start from the start of the first item in the list /// @param _count The maximum number of items to return function scan(bytes32 _namespace, uint256 _start, uint256 _count) override external view returns (DepositQueueValue[] memory entries, uint256 nextIndex) { entries = new DepositQueueValue[](_count); nextIndex = _start; uint256 total = 0; // If nextIndex is 0, begin scan at the start of the list if (nextIndex == 0) { uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); uint256 start = uint64(data >> startOffset); nextIndex = start; } while (total < _count && nextIndex != 0) { uint256 packedValue = getUint(keccak256(abi.encodePacked(_namespace, ".item", nextIndex))); entries[total] = _unpackItem(packedValue); nextIndex = getUint(keccak256(abi.encodePacked(_namespace, ".next", nextIndex))); total++; } assembly { mstore(entries, total) } return (entries, nextIndex); } } ================================================ FILE: contracts/contract/util/LinkedListStorageHelper.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; pragma abicoder v2; import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol"; import {LinkedListStorage} from "./LinkedListStorage.sol"; /// @notice A linked list storage helper to test internal functions contract LinkedListStorageHelper is LinkedListStorage { // Construct constructor(RocketStorageInterface _rocketStorageAddress) LinkedListStorage(_rocketStorageAddress) { version = 1; } /// @notice Add an item to the end of the list. Requires that the item does not exist in the list /// @param _namespace defines the queue to be used /// @param _item the deposit queue item function enqueueItem(bytes32 _namespace, DepositQueueValue memory _item) public override { _enqueueItem(_namespace, _item); } /// @notice Remove an item from the start of a queue and return it. Requires that the queue is not empty /// @param _namespace defines the queue to be used function dequeueItem(bytes32 _namespace) public virtual override returns (DepositQueueValue memory item) { return _dequeueItem(_namespace); } /// @notice Removes an item from a queue. Requires that the item exists in the queue /// @param _namespace to be used /// @param _key to be removed from the queue function removeItem(bytes32 _namespace, DepositQueueKey memory _key) public virtual override { return _removeItem(_namespace, _key); } function packItem(DepositQueueValue memory _item) public pure returns (uint256 packed) { return _packItem(_item); } function unpackItem(uint256 _item) public pure returns (DepositQueueValue memory item) { return _unpackItem(_item); } } ================================================ FILE: contracts/contract/util/SSZ.sol ================================================ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; /// @dev Set of utilities for working with SSZ serialisation and merkleisation library SSZ { struct Path { uint256 _data; } /// @dev Decodes a Path to a full gindex function toIndex(Path memory _path) internal pure returns (uint256) { uint256 pathLength = uint8(_path._data); uint256 anchor = uint256(1) << pathLength; return (_path._data >> 8) | anchor; } /// @dev Extracts the length component from a Path function length(Path memory _path) internal pure returns (uint8) { return uint8(_path._data); } /// @dev Constructs a Path from a given gindex and length function from(uint248 _gindex, uint8 _length) internal pure returns (Path memory) { require(_gindex < 2 ** _length, "Gindex exceeds length"); return Path((uint256(_gindex) << 8) | uint256(_length)); } /// @dev Constructs a Path into a list field function intoList(uint248 _index, uint8 _log2Length) internal pure returns (Path memory) { require(_index < 2 ** _log2Length, "Index exceeds length"); return Path((uint256(_index) << 8) | uint256(_log2Length + 1)); } /// @dev Constructs a Path into a vector field function intoVector(uint248 _index, uint8 _log2Length) internal pure returns (Path memory) { require(_index < 2 ** _log2Length, "Index exceeds length"); return Path((uint256(_index) << 8) | uint256(_log2Length)); } /// @dev Concatenates two Paths function concat(Path memory _left, Path memory _right) internal pure returns (Path memory) { uint8 lenA = uint8(_left._data); uint8 lenB = uint8(_right._data); unchecked { // Prevent overflow of length into path require(lenA + lenB <= 248, "Path too long"); _left._data = (_left._data - lenA) << lenB; _left._data += _right._data + lenA; } return _left; } /// @dev Interprets a big-ending uint256 as a little-endian encodes bytes32 value function toLittleEndian(uint256 v) internal pure returns (bytes32) { v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); v = (v >> 128) | (v << 128); return bytes32(v); } /// @dev Performs SSZ merkleisation of a pubkey value function merkleisePubkey(bytes memory pubkey) internal view returns (bytes32 ret) { require(pubkey.length == 48, "Invalid pubkey length"); assembly { mstore(0x00, mload(add(0x20, pubkey))) let right := and(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000, mload(add(0x40, pubkey))) mstore(0x20, right) let result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) if iszero(result) { revert(0,0) } ret := mload(0x00) } } /// @dev Concatenates two bytes32 values and returns a SHA256 of the result function efficientSha256(bytes32 _left, bytes32 _right) internal view returns (bytes32 ret) { assembly { mstore(0x00, _left) mstore(0x20, _right) let result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) if iszero(result) { revert(0,0) } ret := mload(0x00) } } /// @dev Restores a merkle root from a merkle proof /// @param _leaf The SSZ merkleised leaf node /// @param _gindex The gindex of the proof /// @param _witnesses The proof witnesses function restoreMerkleRoot(bytes32 _leaf, uint256 _gindex, bytes32[] memory _witnesses) internal view returns (bytes32) { // Check for correct number of witnesses require(2 ** (_witnesses.length + 1) > _gindex, "Invalid witness length"); bytes32 value = _leaf; uint256 i = 0; while (_gindex != 1) { if (_gindex % 2 == 1) { value = efficientSha256(_witnesses[i], value); } else { value = efficientSha256(value, _witnesses[i]); } _gindex /= 2; unchecked { i++; } } return value; } } ================================================ FILE: contracts/contract/util/SafeERC20.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity >0.5.0 <0.9.0; import "../../interface/util/IERC20.sol"; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/Address.sol"; /** * @title SafeERC20 * @dev Wrappers around ERC20 operations that throw on failure (when the token * contract returns false). Tokens that return no value (and instead revert or * throw on failure) are also supported, non-reverting calls are assumed to be * successful. * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. */ library SafeERC20 { using SafeMath for uint256; using Address for address; function safeTransfer(IERC20 token, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); } function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); } /** * @dev Deprecated. This function has issues similar to the ones found in * {IERC20-approve}, and its usage is discouraged. * * Whenever possible, use {safeIncreaseAllowance} and * {safeDecreaseAllowance} instead. */ function safeApprove(IERC20 token, address spender, uint256 value) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' // solhint-disable-next-line max-line-length require((value == 0) || (token.allowance(address(this), spender) == 0), "SafeERC20: approve from non-zero to non-zero allowance" ); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 newAllowance = token.allowance(address(this), spender).add(value); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); } function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 newAllowance = token.allowance(address(this), spender).sub(value, "SafeERC20: decreased allowance below zero"); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); } /** * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement * on the return value: the return value is optional (but if data is returned, it must not be false). * @param token The token targeted by the call. * @param data The call data (encoded using abi.encode or one of its variants). */ function _callOptionalReturn(IERC20 token, bytes memory data) private { // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that // the target address contains contract code and also asserts for success in the low-level call. bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); if (returndata.length > 0) { // Return data is optional // solhint-disable-next-line max-line-length require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); } } } ================================================ FILE: contracts/contract/util/SafeMath.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity >0.5.0 <0.9.0; /** * @dev Wrappers over Solidity's arithmetic operations with added overflow * checks. * * Arithmetic operations in Solidity wrap on overflow. This can easily result * in bugs, because programmers usually assume that an overflow raises an * error, which is the standard behavior in high level programming languages. * `SafeMath` restores this intuition by reverting the transaction when an * operation overflows. * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. */ library SafeMath { /** * @dev Returns the addition of two unsigned integers, with an overflow flag. * * _Available since v3.4._ */ function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { uint256 c = a + b; if (c < a) return (false, 0); return (true, c); } /** * @dev Returns the substraction of two unsigned integers, with an overflow flag. * * _Available since v3.4._ */ function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { if (b > a) return (false, 0); return (true, a - b); } /** * @dev Returns the multiplication of two unsigned integers, with an overflow flag. * * _Available since v3.4._ */ function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 if (a == 0) return (true, 0); uint256 c = a * b; if (c / a != b) return (false, 0); return (true, c); } /** * @dev Returns the division of two unsigned integers, with a division by zero flag. * * _Available since v3.4._ */ function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { if (b == 0) return (false, 0); return (true, a / b); } /** * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. * * _Available since v3.4._ */ function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { if (b == 0) return (false, 0); return (true, a % b); } /** * @dev Returns the addition of two unsigned integers, reverting on * overflow. * * Counterpart to Solidity's `+` operator. * * Requirements: * * - Addition cannot overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath: addition overflow"); return c; } /** * @dev Returns the subtraction of two unsigned integers, reverting on * overflow (when the result is negative). * * Counterpart to Solidity's `-` operator. * * Requirements: * * - Subtraction cannot overflow. */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a, "SafeMath: subtraction overflow"); return a - b; } /** * @dev Returns the multiplication of two unsigned integers, reverting on * overflow. * * Counterpart to Solidity's `*` operator. * * Requirements: * * - Multiplication cannot overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { if (a == 0) return 0; uint256 c = a * b; require(c / a == b, "SafeMath: multiplication overflow"); return c; } /** * @dev Returns the integer division of two unsigned integers, reverting on * division by zero. The result is rounded towards zero. * * Counterpart to Solidity's `/` operator. Note: this function uses a * `revert` opcode (which leaves remaining gas untouched) while Solidity * uses an invalid opcode to revert (consuming all remaining gas). * * Requirements: * * - The divisor cannot be zero. */ function div(uint256 a, uint256 b) internal pure returns (uint256) { require(b > 0, "SafeMath: division by zero"); return a / b; } /** * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), * reverting when dividing by zero. * * Counterpart to Solidity's `%` operator. This function uses a `revert` * opcode (which leaves remaining gas untouched) while Solidity uses an * invalid opcode to revert (consuming all remaining gas). * * Requirements: * * - The divisor cannot be zero. */ function mod(uint256 a, uint256 b) internal pure returns (uint256) { require(b > 0, "SafeMath: modulo by zero"); return a % b; } /** * @dev Returns the subtraction of two unsigned integers, reverting with custom message on * overflow (when the result is negative). * * CAUTION: This function is deprecated because it requires allocating memory for the error * message unnecessarily. For custom revert reasons use {trySub}. * * Counterpart to Solidity's `-` operator. * * Requirements: * * - Subtraction cannot overflow. */ function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b <= a, errorMessage); return a - b; } /** * @dev Returns the integer division of two unsigned integers, reverting with custom message on * division by zero. The result is rounded towards zero. * * CAUTION: This function is deprecated because it requires allocating memory for the error * message unnecessarily. For custom revert reasons use {tryDiv}. * * Counterpart to Solidity's `/` operator. Note: this function uses a * `revert` opcode (which leaves remaining gas untouched) while Solidity * uses an invalid opcode to revert (consuming all remaining gas). * * Requirements: * * - The divisor cannot be zero. */ function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b > 0, errorMessage); return a / b; } /** * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), * reverting with custom message when dividing by zero. * * CAUTION: This function is deprecated because it requires allocating memory for the error * message unnecessarily. For custom revert reasons use {tryMod}. * * Counterpart to Solidity's `%` operator. This function uses a `revert` * opcode (which leaves remaining gas untouched) while Solidity uses an * invalid opcode to revert (consuming all remaining gas). * * Requirements: * * - The divisor cannot be zero. */ function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { require(b > 0, errorMessage); return a % b; } } ================================================ FILE: contracts/interface/RocketStorageInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketStorageInterface { // Deploy status function getDeployedStatus() external view returns (bool); // Guardian function getGuardian() external view returns(address); function setGuardian(address _newAddress) external; function confirmGuardian() external; // Getters function getAddress(bytes32 _key) external view returns (address); function getUint(bytes32 _key) external view returns (uint); function getString(bytes32 _key) external view returns (string memory); function getBytes(bytes32 _key) external view returns (bytes memory); function getBool(bytes32 _key) external view returns (bool); function getInt(bytes32 _key) external view returns (int); function getBytes32(bytes32 _key) external view returns (bytes32); // Setters function setAddress(bytes32 _key, address _value) external; function setUint(bytes32 _key, uint _value) external; function setString(bytes32 _key, string calldata _value) external; function setBytes(bytes32 _key, bytes calldata _value) external; function setBool(bytes32 _key, bool _value) external; function setInt(bytes32 _key, int _value) external; function setBytes32(bytes32 _key, bytes32 _value) external; // Deleters function deleteAddress(bytes32 _key) external; function deleteUint(bytes32 _key) external; function deleteString(bytes32 _key) external; function deleteBytes(bytes32 _key) external; function deleteBool(bytes32 _key) external; function deleteInt(bytes32 _key) external; function deleteBytes32(bytes32 _key) external; // Arithmetic function addUint(bytes32 _key, uint256 _amount) external; function subUint(bytes32 _key, uint256 _amount) external; // Protected storage function getNodeWithdrawalAddress(address _nodeAddress) external view returns (address); function getNodePendingWithdrawalAddress(address _nodeAddress) external view returns (address); function setWithdrawalAddress(address _nodeAddress, address _newWithdrawalAddress, bool _confirm) external; function confirmWithdrawalAddress(address _nodeAddress) external; } ================================================ FILE: contracts/interface/RocketVaultInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "./util/IERC20Burnable.sol"; interface RocketVaultInterface { function balanceOf(string memory _networkContractName) external view returns (uint256); function depositEther() external payable; function withdrawEther(uint256 _amount) external; function depositToken(string memory _networkContractName, IERC20 _tokenAddress, uint256 _amount) external; function withdrawToken(address _withdrawalAddress, IERC20 _tokenAddress, uint256 _amount) external; function balanceOfToken(string memory _networkContractName, IERC20 _tokenAddress) external view returns (uint256); function transferToken(string memory _networkContractName, IERC20 _tokenAddress, uint256 _amount) external; function burnToken(IERC20Burnable _tokenAddress, uint256 _amount) external; } ================================================ FILE: contracts/interface/RocketVaultWithdrawerInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketVaultWithdrawerInterface { function receiveVaultWithdrawalETH() external payable; } ================================================ FILE: contracts/interface/auction/RocketAuctionManagerInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketAuctionManagerInterface { function getTotalRPLBalance() external view returns (uint256); function getAllottedRPLBalance() external view returns (uint256); function getRemainingRPLBalance() external view returns (uint256); function getLotCount() external view returns (uint256); function getLotExists(uint256 _index) external view returns (bool); function getLotStartBlock(uint256 _index) external view returns (uint256); function getLotEndBlock(uint256 _index) external view returns (uint256); function getLotStartPrice(uint256 _index) external view returns (uint256); function getLotReservePrice(uint256 _index) external view returns (uint256); function getLotTotalRPLAmount(uint256 _index) external view returns (uint256); function getLotTotalBidAmount(uint256 _index) external view returns (uint256); function getLotAddressBidAmount(uint256 _index, address _bidder) external view returns (uint256); function getLotRPLRecovered(uint256 _index) external view returns (bool); function getLotPriceAtBlock(uint256 _index, uint256 _block) external view returns (uint256); function getLotPriceAtCurrentBlock(uint256 _index) external view returns (uint256); function getLotPriceByTotalBids(uint256 _index) external view returns (uint256); function getLotCurrentPrice(uint256 _index) external view returns (uint256); function getLotClaimedRPLAmount(uint256 _index) external view returns (uint256); function getLotRemainingRPLAmount(uint256 _index) external view returns (uint256); function getLotIsCleared(uint256 _index) external view returns (bool); function createLot() external; function placeBid(uint256 _lotIndex) external payable; function claimBid(uint256 _lotIndex) external; function recoverUnclaimedRPL(uint256 _lotIndex) external; } ================================================ FILE: contracts/interface/casper/DepositInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface DepositInterface { function deposit(bytes calldata _pubkey, bytes calldata _withdrawalCredentials, bytes calldata _signature, bytes32 _depositDataRoot) external payable; } ================================================ FILE: contracts/interface/dao/RocketDAOProposalInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProposalInterface { // Possible states that a proposal may be in enum ProposalState { Pending, Active, Cancelled, Defeated, Succeeded, Expired, Executed } function getTotal() external view returns (uint256); function getDAO(uint256 _proposalID) external view returns (string memory); function getProposer(uint256 _proposalID) external view returns (address); function getMessage(uint256 _proposalID) external view returns (string memory); function getStart(uint256 _proposalID) external view returns (uint256); function getEnd(uint256 _proposalID) external view returns (uint256); function getExpires(uint256 _proposalID) external view returns (uint256); function getCreated(uint256 _proposalID) external view returns (uint256); function getVotesFor(uint256 _proposalID) external view returns (uint256); function getVotesAgainst(uint256 _proposalID) external view returns (uint256); function getVotesRequired(uint256 _proposalID) external view returns (uint256); function getCancelled(uint256 _proposalID) external view returns (bool); function getExecuted(uint256 _proposalID) external view returns (bool); function getPayload(uint256 _proposalID) external view returns (bytes memory); function getReceiptHasVoted(uint256 _proposalID, address _nodeAddress) external view returns (bool); function getReceiptSupported(uint256 _proposalID, address _nodeAddress) external view returns (bool); function getState(uint256 _proposalID) external view returns (ProposalState); function add(address _member, string memory _dao, string memory _message, uint256 _startBlock, uint256 _durationBlocks, uint256 _expiresBlocks, uint256 _votesRequired, bytes memory _payload) external returns (uint256); function vote(address _member, uint256 _votes, uint256 _proposalID, bool _support) external; function cancel(address _member, uint256 _proposalID) external; function execute(uint256 _proposalID) external; } ================================================ FILE: contracts/interface/dao/node/RocketDAONodeTrustedActionsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedActionsInterface { function actionJoin() external; function actionJoinRequired(address _nodeAddress) external; function actionLeave(address _rplBondRefundAddress) external; function actionKick(address _nodeAddress, uint256 _rplFine) external; function actionChallengeMake(address _nodeAddress) external payable; function actionChallengeDecide(address _nodeAddress) external; } ================================================ FILE: contracts/interface/dao/node/RocketDAONodeTrustedInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedInterface { function getBootstrapModeDisabled() external view returns (bool); function getMemberQuorumVotesRequired() external view returns (uint256); function getMemberAt(uint256 _index) external view returns (address); function getMemberCount() external view returns (uint256); function getMemberMinRequired() external view returns (uint256); function getMemberIsValid(address _nodeAddress) external view returns (bool); function getMemberLastProposalTime(address _nodeAddress) external view returns (uint256); function getMemberID(address _nodeAddress) external view returns (string memory); function getMemberUrl(address _nodeAddress) external view returns (string memory); function getMemberJoinedTime(address _nodeAddress) external view returns (uint256); function getMemberProposalExecutedTime(string memory _proposalType, address _nodeAddress) external view returns (uint256); function getMemberRPLBondAmount(address _nodeAddress) external view returns (uint256); function getMemberIsChallenged(address _nodeAddress) external view returns (bool); function getMemberUnbondedValidatorCount(address _nodeAddress) external view returns (uint256); function incrementMemberUnbondedValidatorCount(address _nodeAddress) external; function decrementMemberUnbondedValidatorCount(address _nodeAddress) external; function bootstrapMember(string memory _id, string memory _url, address _nodeAddress) external; function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) external; function bootstrapSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) external; function bootstrapUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) external; function bootstrapDisable(bool _confirmDisableBootstrapMode) external; function memberJoinRequired(string memory _id, string memory _url) external; } ================================================ FILE: contracts/interface/dao/node/RocketDAONodeTrustedProposalsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedProposalsInterface { function propose(string memory _proposalMessage, bytes memory _payload) external returns (uint256); function vote(uint256 _proposalID, bool _support) external; function cancel(uint256 _proposalID) external; function execute(uint256 _proposalID) external; function proposalInvite(string memory _id, string memory _url, address _nodeAddress) external; function proposalLeave(address _nodeAddress) external; function proposalKick(address _nodeAddress, uint256 _rplFine) external; function proposalSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) external; function proposalSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) external; function proposalUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) external; } ================================================ FILE: contracts/interface/dao/node/RocketDAONodeTrustedUpgradeInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAONodeTrustedUpgradeInterface { enum UpgradeProposalState { Pending, // Upgrade proposal is in the delay period Succeeded, // Upgrade proposal can be executed immediately Vetoed, // Upgrade was vetoed by the security council Executed // Upgrade was executed } function upgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) external; function veto(uint256 _upgradeProposalID) external; function execute(uint256 _upgradeProposalID) external; function bootstrapUpgrade(string memory _type, string memory _name, string memory _contractAbi, address _contractAddress) external; function getTotal() external view returns (uint256); function getState(uint256 _upgradeProposalID) external view returns (UpgradeProposalState); function getEnd(uint256 _upgradeProposalID) external view returns (uint256); function getExecuted(uint256 _upgradeProposalID) external view returns (bool); function getVetoed(uint256 _upgradeProposalID) external view returns (bool); function getType(uint256 _upgradeProposalID) external view returns (bytes32); function getName(uint256 _upgradeProposalID) external view returns (string memory); function getUpgradeAddress(uint256 _upgradeProposalID) external view returns (address); function getUpgradeABI(uint256 _upgradeProposalID) external view returns (string memory); } ================================================ FILE: contracts/interface/dao/node/settings/RocketDAONodeTrustedSettingsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedSettingsInterface { function getSettingUint(string memory _settingPath) external view returns (uint256); function setSettingUint(string memory _settingPath, uint256 _value) external; function getSettingBool(string memory _settingPath) external view returns (bool); function setSettingBool(string memory _settingPath, bool _value) external; } ================================================ FILE: contracts/interface/dao/node/settings/RocketDAONodeTrustedSettingsMembersInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedSettingsMembersInterface { function getQuorum() external view returns (uint256); function getRPLBond() external view returns(uint256); function getMinipoolUnbondedMax() external view returns(uint256); function getMinipoolUnbondedMinFee() external view returns(uint256); function getChallengeCooldown() external view returns(uint256); function getChallengeWindow() external view returns(uint256); function getChallengeCost() external view returns(uint256); } ================================================ FILE: contracts/interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedSettingsMinipoolInterface { function getScrubPeriod() external view returns(uint256); function getPromotionScrubPeriod() external view returns(uint256); function getScrubQuorum() external view returns(uint256); function getCancelBondReductionQuorum() external view returns(uint256); function getScrubPenaltyEnabled() external view returns(bool); function isWithinBondReductionWindow(uint256 _time) external view returns (bool); function getBondReductionWindowStart() external view returns (uint256); function getBondReductionWindowLength() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/node/settings/RocketDAONodeTrustedSettingsProposalsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAONodeTrustedSettingsProposalsInterface { function getCooldownTime() external view returns(uint256); function getVoteTime() external view returns(uint256); function getVoteDelayTime() external view returns(uint256); function getExecuteTime() external view returns(uint256); function getActionTime() external view returns(uint256); } ================================================ FILE: contracts/interface/dao/node/settings/RocketDAONodeTrustedSettingsRewardsInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAONodeTrustedSettingsRewardsInterface { function getNetworkEnabled(uint256 _network) external view returns (bool); } ================================================ FILE: contracts/interface/dao/protocol/RocketDAOProtocolActionsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolActionsInterface { } ================================================ FILE: contracts/interface/dao/protocol/RocketDAOProtocolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../../types/SettingType.sol"; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolInterface { function getMemberLastProposalTime(address _nodeAddress) external view returns (uint256); function getBootstrapModeDisabled() external view returns (bool); function bootstrapSettingMulti(string[] memory _settingContractNames, string[] memory _settingPaths, SettingType[] memory _types, bytes[] memory _values) external; function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) external; function bootstrapSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) external; function bootstrapSettingAddress(string memory _settingContractName, string memory _settingPath, address _value) external; function bootstrapSettingAddressList(string memory _settingContractName, string memory _settingPath, address[] calldata _value) external; function bootstrapSettingClaimers(uint256 _trustedNodePerc, uint256 _protocolPerc, uint256 _nodePerc) external; function bootstrapSpendTreasury(string memory _invoiceID, address _recipientAddress, uint256 _amount) external; function bootstrapTreasuryNewContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) external; function bootstrapTreasuryUpdateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) external; function bootstrapSecurityInvite(string memory _id, address _memberAddress) external; function bootstrapSecurityKick(address _memberAddress) external; function bootstrapDisable(bool _confirmDisableBootstrapMode) external; function bootstrapEnableGovernance() external; } ================================================ FILE: contracts/interface/dao/protocol/RocketDAOProtocolProposalInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../../types/SettingType.sol"; import "./RocketDAOProtocolVerifierInterface.sol"; interface RocketDAOProtocolProposalInterface { // Possible states that a proposal may be in enum ProposalState { Pending, ActivePhase1, ActivePhase2, Destroyed, Vetoed, QuorumNotMet, Defeated, Succeeded, Expired, Executed } enum VoteDirection { NoVote, Abstain, For, Against, AgainstWithVeto } function getTotal() external view returns (uint256); function getProposer(uint256 _proposalID) external view returns (address); function getMessage(uint256 _proposalID) external view returns (string memory); function getStart(uint256 _proposalID) external view returns (uint256); function getPhase1End(uint256 _proposalID) external view returns (uint256); function getPhase2End(uint256 _proposalID) external view returns (uint256); function getExpires(uint256 _proposalID) external view returns (uint256); function getCreated(uint256 _proposalID) external view returns (uint256); function getVotingPowerFor(uint256 _proposalID) external view returns (uint256); function getVotingPowerAgainst(uint256 _proposalID) external view returns (uint256); function getVotingPowerVeto(uint256 _proposalID) external view returns (uint256); function getVotingPowerAbstained(uint256 _proposalID) external view returns (uint256); function getVotingPowerRequired(uint256 _proposalID) external view returns (uint256); function getDestroyed(uint256 _proposalID) external view returns (bool); function getFinalised(uint256 _proposalID) external view returns (bool); function getExecuted(uint256 _proposalID) external view returns (bool); function getVetoQuorum(uint256 _proposalID) external view returns (uint256); function getVetoed(uint256 _proposalID) external view returns (bool); function getPayload(uint256 _proposalID) external view returns (bytes memory); function getReceiptHasVoted(uint256 _proposalID, address _nodeAddress) external view returns (bool); function getReceiptHasVotedPhase1(uint256 _proposalID, address _nodeAddress) external view returns (bool); function getReceiptDirection(uint256 _proposalID, address _nodeAddress) external view returns (VoteDirection); function getState(uint256 _proposalID) external view returns (ProposalState); function getProposalBlock(uint256 _proposalID) external view returns (uint256); function getProposalVetoQuorum(uint256 _proposalID) external view returns (uint256); function propose(string memory _proposalMessage, bytes memory _payload, uint32 _blockNumber, Types.Node[] calldata _treeNodes) external returns (uint256); function vote(uint256 _proposalID, VoteDirection _vote, uint256 _votingPower, uint256 _nodeIndex, Types.Node[] calldata _witness) external; function overrideVote(uint256 _proposalID, VoteDirection _voteDirection) external; function finalise(uint256 _proposalID) external; function execute(uint256 _proposalID) external; function destroy(uint256 _proposalID) external; } ================================================ FILE: contracts/interface/dao/protocol/RocketDAOProtocolProposalsInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../../types/SettingType.sol"; interface RocketDAOProtocolProposalsInterface { function proposalSettingMulti(string[] memory _settingContractNames, string[] memory _settingPaths, SettingType[] memory _types, bytes[] memory _data) external; function proposalSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) external; function proposalSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) external; function proposalSettingAddress(string memory _settingContractName, string memory _settingPath, address _value) external; function proposalSettingAddressList(string memory _settingContractName, string memory _settingPath, address[] calldata _value) external; function proposalSettingRewardsClaimers(uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent) external; function proposalTreasuryOneTimeSpend(string memory _invoiceID, address _recipientAddress, uint256 _amount) external; function proposalTreasuryNewContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) external; function proposalTreasuryUpdateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) external; function proposalSecurityInvite(string memory _id, address _memberAddress) external; function proposalSecurityKick(address _memberAddress) external; function proposalSecurityKickMulti(address[] calldata _memberAddresses) external; function proposalSecurityReplace(address _existingMemberAddress, string calldata _id, address _newMemberAddress) external; } ================================================ FILE: contracts/interface/dao/protocol/RocketDAOProtocolVerifierInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; interface Types { enum ChallengeState { Unchallenged, Challenged, Responded, Paid } struct Proposal { address proposer; uint32 blockNumber; uint128 nodeCount; bytes32 hash; uint256 sum; } struct Node { uint256 sum; bytes32 hash; } struct Leaf { address nodeAddress; uint256 effectiveRpl; } } interface RocketDAOProtocolVerifierInterface { function getDefeatIndex(uint256 _proposalID) external view returns (uint256); function getProposalBond(uint256 _proposalID) external view returns (uint256); function getChallengeBond(uint256 _proposalID) external view returns (uint256); function getChallengePeriod(uint256 _proposalID) external view returns (uint256); function getDepthPerRound() external pure returns (uint256); function submitProposalRoot(uint256 _proposalId, address _proposer, uint32 _blockNumber, Types.Node[] memory _treeNodes) external; function burnProposalBond(uint256 _proposalID) external; function createChallenge(uint256 _proposalID, uint256 _index, Types.Node calldata _node, Types.Node[] calldata _witness) external; function submitRoot(uint256 propId, uint256 index, Types.Node[] memory nodes) external; function getChallengeState(uint256 _proposalID, uint256 _index) external view returns (Types.ChallengeState); function verifyVote(address _voter, uint256 _nodeIndex, uint256 _proposalID, uint256 _votingPower, Types.Node[] calldata _witness) external view returns (bool); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsAuctionInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsAuctionInterface { function getCreateLotEnabled() external view returns (bool); function getBidOnLotEnabled() external view returns (bool); function getLotMinimumEthValue() external view returns (uint256); function getLotMaximumEthValue() external view returns (uint256); function getLotDuration() external view returns (uint256); function getStartingPriceRatio() external view returns (uint256); function getReservePriceRatio() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsDepositInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsDepositInterface { function getDepositEnabled() external view returns (bool); function getAssignDepositsEnabled() external view returns (bool); function getMinimumDeposit() external view returns (uint256); function getMaximumDepositPoolSize() external view returns (uint256); function getMaximumDepositAssignments() external view returns (uint256); function getMaximumDepositSocialisedAssignments() external view returns (uint256); function getDepositFee() external view returns (uint256); function getExpressQueueRate() external view returns (uint256); function getExpressQueueTicketsBaseProvision() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsInflationInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsInflationInterface { function getInflationIntervalRate() external view returns (uint256); function getInflationIntervalStartTime() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsInterface { function getSettingUint(string memory _settingPath) external view returns (uint256); function setSettingUint(string memory _settingPath, uint256 _value) external; function getSettingBool(string memory _settingPath) external view returns (bool); function setSettingBool(string memory _settingPath, bool _value) external; function getSettingAddress(string memory _settingPath) external view returns (address); function setSettingAddress(string memory _settingPath, address _value) external; function setSettingAddressList(string memory _settingPath, address[] calldata _value) external; function getSettingAddressList(string memory _settingPath) external view returns (address[] memory); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMegapoolInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAOProtocolSettingsMegapoolInterface { function initialise() external; function getTimeBeforeDissolve() external view returns (uint256); function getDissolvePenalty() external view returns (uint256); function getMaximumEthPenalty() external view returns (uint256); function getNotifyThreshold() external view returns (uint256); function getLateNotifyFine() external view returns (uint256); function getUserDistributeDelay() external view returns (uint256); function getUserDistributeDelayWithShortfall() external view returns (uint256); function getPenaltyThreshold() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../../../../types/MinipoolDeposit.sol"; interface RocketDAOProtocolSettingsMinipoolInterface { function getLaunchBalance() external view returns (uint256); function getPreLaunchValue() external pure returns (uint256); function getDepositUserAmount(MinipoolDeposit _depositType) external view returns (uint256); function getFullDepositUserAmount() external view returns (uint256); function getHalfDepositUserAmount() external view returns (uint256); function getVariableDepositAmount() external view returns (uint256); function getSubmitWithdrawableEnabled() external view returns (bool); function getBondReductionEnabled() external view returns (bool); function getLaunchTimeout() external view returns (uint256); function getMaximumCount() external view returns (uint256); function isWithinUserDistributeWindow(uint256 _time) external view returns (bool); function hasUserDistributeWindowPassed(uint256 _time) external view returns (bool); function getUserDistributeWindowStart() external view returns (uint256); function getUserDistributeWindowLength() external view returns (uint256); function getMaximumPenaltyCount() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsNetworkInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsNetworkInterface { function getNodeConsensusThreshold() external view returns (uint256); function getNodePenaltyThreshold() external view returns (uint256); function getPerPenaltyRate() external view returns (uint256); function getSubmitBalancesEnabled() external view returns (bool); function getSubmitBalancesFrequency() external view returns (uint256); function getSubmitPricesEnabled() external view returns (bool); function getSubmitPricesFrequency() external view returns (uint256); function getMinimumNodeFee() external view returns (uint256); function getTargetNodeFee() external view returns (uint256); function getMaximumNodeFee() external view returns (uint256); function getNodeFeeDemandRange() external view returns (uint256); function getTargetRethCollateralRate() external view returns (uint256); function getRethDepositDelay() external view returns (uint256); function getSubmitRewardsEnabled() external view returns (bool); function getMaxNodeShareSecurityCouncilAdder() external view returns (uint256); function getVoterShare() external view returns (uint256); function getProtocolDAOShare() external view returns (uint256); function getNodeShare() external view returns (uint256); function getNodeShareSecurityCouncilAdder() external view returns (uint256); function getRethCommission() external view returns (uint256); function getEffectiveVoterShare() external view returns (uint256); function getEffectiveNodeShare() external view returns (uint256); function getAllowListedControllers() external view returns (address[] memory); function getMaxRethDelta() external view returns (uint256); function isAllowListedController(address _address) external view returns (bool); function setNodeShareSecurityCouncilAdder(uint256 _value) external; function setNodeCommissionShare(uint256 _value) external; function setVoterShare(uint256 _value) external; function setProtocolDAOShare(uint256 _value) external; } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsNodeInterface { function getRegistrationEnabled() external view returns (bool); function getSmoothingPoolRegistrationEnabled() external view returns (bool); function getDepositEnabled() external view returns (bool); function getVacantMinipoolsEnabled() external view returns (bool); function getMaximumStakeForVotingPower() external view returns (uint256); function getReducedBond() external view returns (uint256); function getBaseBondArray() external view returns (uint256[] memory); function getUnstakingPeriod() external view returns (uint256); function getWithdrawalCooldown() external view returns (uint256); function getMinimumLegacyRPLStake() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsProposalsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOProtocolSettingsProposalsInterface { function getVotePhase1Time() external view returns(uint256); function getVotePhase2Time() external view returns(uint256); function getVoteDelayTime() external view returns(uint256); function getExecuteTime() external view returns(uint256); function getProposalBond() external view returns(uint256); function getChallengeBond() external view returns(uint256); function getChallengePeriod() external view returns(uint256); function getProposalQuorum() external view returns (uint256); function getProposalVetoQuorum() external view returns (uint256); function getProposalMaxBlockAge() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAOProtocolSettingsRewardsInterface { function setSettingRewardsClaimers(uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent) external; function getRewardsClaimerPerc(string memory _contractName) external view returns (uint256); function getRewardsClaimersPerc() external view returns (uint256 _trustedNodePercent, uint256 _protocolPercent, uint256 _nodePercent); function getRewardsClaimersTrustedNodePerc() external view returns (uint256); function getRewardsClaimersProtocolPerc() external view returns (uint256); function getRewardsClaimersNodePerc() external view returns (uint256); function getRewardsClaimersTimeUpdated() external view returns (uint256); function getRewardsClaimIntervalPeriods() external view returns (uint256); function getRewardsClaimIntervalTime() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsSecurityInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAOProtocolSettingsSecurityInterface { function getQuorum() external view returns (uint256); function getLeaveTime() external view returns (uint256); function getVoteTime() external view returns(uint256); function getExecuteTime() external view returns(uint256); function getActionTime() external view returns (uint256); function getUpgradeVetoQuorum() external view returns (uint256); function getUpgradeDelay() external view returns (uint256); } ================================================ FILE: contracts/interface/dao/security/RocketDAOSecurityActionsInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDAOSecurityActionsInterface { function actionKick(address _nodeAddress) external; function actionKickMulti(address[] calldata _nodeAddresses) external; function actionJoin() external; function actionRequestLeave() external; function actionLeave() external; } ================================================ FILE: contracts/interface/dao/security/RocketDAOSecurityInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketDAOSecurityInterface { function getMemberQuorumVotesRequired() external view returns (uint256); function getMemberIsValid(address _nodeAddress) external view returns (bool); function getMemberAt(uint256 _index) external view returns (address); function getMemberCount() external view returns (uint256); function getMemberID(address _nodeAddress) external view returns (string memory); function getMemberJoinedTime(address _nodeAddress) external view returns (uint256); function getMemberProposalExecutedTime(string memory _proposalType, address _nodeAddress) external view returns (uint256); } ================================================ FILE: contracts/interface/dao/security/RocketDAOSecurityProposalsInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../../types/SettingType.sol"; interface RocketDAOSecurityProposalsInterface { function propose(string memory _proposalMessage, bytes memory _payload) external returns (uint256); function vote(uint256 _proposalID, bool _support) external; function cancel(uint256 _proposalID) external; function execute(uint256 _proposalID) external; function proposalSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) external; function proposalSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) external; function proposalSettingAddress(string memory _settingContractName, string memory _settingPath, address _value) external; function proposalInvite(string memory _id, address _memberAddress) external; function proposalKick(address _memberAddress) external; function proposalKickMulti(address[] calldata _memberAddresses) external; function proposalReplace(address _existingMemberAddress, string calldata _newMemberId, address _newMemberAddress) external; } ================================================ FILE: contracts/interface/dao/security/RocketDAOSecurityUpgradeInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../../types/SettingType.sol"; interface RocketDAOSecurityUpgradeInterface { function proposeVeto(string memory _proposalMessage, uint256 _upgradeProposalId) external returns (uint256); function vote(uint256 _proposalID, bool _support) external; function cancel(uint256 _proposalID) external; function execute(uint256 _proposalID) external; function proposalVeto(uint256 _upgradeProposalId) external; } ================================================ FILE: contracts/interface/deposit/RocketDepositPoolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketDepositPoolInterface { function getBalance() external view returns (uint256); function getNodeBalance() external view returns (uint256); function getUserBalance() external view returns (int256); function getNodeCreditBalance(address _nodeAddress) external view returns (uint256); function getExcessBalance() external view returns (uint256); function deposit() external payable; function getMaximumDepositAmount() external view returns (uint256); function nodeDeposit(uint256 _totalAmount) external payable; function recycleDissolvedDeposit() external payable; function recycleExcessCollateral() external payable; function recycleLiquidatedStake() external payable; function maybeAssignDeposits(uint256 _max) external; function assignDeposits(uint256 _max) external; function withdrawExcessBalance(uint256 _amount) external; function requestFunds(uint256 _bondAmount, uint32 _validatorId, uint256 _amount, bool _useExpressTicket) external; function exitQueue(address _nodeAddress, uint32 _validatorId, bool _expressQueue) external; function applyCredit(address _nodeAddress, uint256 _amount) external; function reduceBond(address _nodeAddress, uint256 _amount) external; function fundsReturned(address _nodeAddress, uint256 _nodeAmount, uint256 _userAmount) external; function withdrawCredit(uint256 _amount) external; function withdrawCreditFor(address _nodeAddress, uint256 _amount) external; function getQueueTop() external view returns (address receiver, bool assignmentPossible, uint256 headMovedBlock); function getQueueIndex() external view returns (uint256); function getMinipoolQueueLength() external view returns (uint256); function getExpressQueueLength() external view returns (uint256); function getStandardQueueLength() external view returns (uint256); function getTotalQueueLength() external view returns (uint256); } ================================================ FILE: contracts/interface/megapool/RocketMegapoolDelegateBaseInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import "../../interface/RocketStorageInterface.sol"; interface RocketMegapoolDelegateBaseInterface { function deprecate() external; function getExpirationTime() external view returns (uint256); } ================================================ FILE: contracts/interface/megapool/RocketMegapoolDelegateInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import "../util/BeaconStateVerifierInterface.sol"; import {RocketMegapoolDelegateBaseInterface} from "./RocketMegapoolDelegateBaseInterface.sol"; import {RocketMegapoolStorageLayout} from "../../contract/megapool/RocketMegapoolStorageLayout.sol"; interface RocketMegapoolDelegateInterface is RocketMegapoolDelegateBaseInterface { struct StateProof { bytes data; } function newValidator(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external; function dequeue(uint32 _validatorId) external; function reduceBond(uint256 _amount) external; function assignFunds(uint32 _validatorId) external payable; function stake(uint32 _validatorId) external; function dissolveValidator(uint32 _validatorId) external; function getNodeAddress() external returns (address); function distribute() external; function claim() external; function repayDebt() external payable; function challengeExit(uint32 _validatorId) external; function notifyNotExit(uint32 _validatorId, uint64 _slotTimestamp) external; function notifyExit(uint32 _validatorId, uint64 _withdrawableEpoch, uint64 _recentEpoch) external; function notifyFinalBalance(uint32 _validatorId, uint64 _amountInGwei, address _caller, uint64 _withdrawalEpoch, uint64 _recentEpoch) external; function applyPenalty(uint256 _amount) external; function getValidatorCount() external view returns (uint32); function getActiveValidatorCount() external view returns (uint32); function getExitingValidatorCount() external view returns (uint32); function getLockedValidatorCount() external view returns (uint32); function getValidatorInfo(uint32 _validatorId) external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory); function getValidatorPubkey(uint32 _validatorId) external view returns (bytes memory); function getValidatorInfoAndPubkey(uint32 _validatorId) external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory info, bytes memory pubkey); function getAssignedValue() external view returns (uint256); function getDebt() external view returns (uint256); function getRefundValue() external view returns (uint256); function getNodeBond() external view returns (uint256); function getUserCapital() external view returns (uint256); function getNodeQueuedBond() external view returns (uint256); function getUserQueuedCapital() external view returns (uint256); function calculatePendingRewards() external view returns (uint256 nodeRewards, uint256 voterRewards, uint256 protocolDAORewards, uint256 rethRewards); function calculateRewards(uint256 _amount) external view returns (uint256 nodeRewards, uint256 voterRewards, uint256 protocolDAORewards, uint256 rethRewards); function getPendingRewards() external view returns (uint256); function getLastDistributionTime() external view returns (uint256); function getNewValidatorBondRequirement() external view returns (uint256); function getWithdrawalCredentials() external view returns (bytes32); } ================================================ FILE: contracts/interface/megapool/RocketMegapoolFactoryInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketMegapoolFactoryInterface { function initialise() external; function getExpectedAddress(address _nodeAddress) external view returns (address); function getMegapoolDeployed(address _nodeAddress) external view returns (bool); function deployContract(address _nodeAddress) external returns (address); function getOrDeployContract(address _nodeAddress) external returns (address); function upgradeDelegate(address _newDelegateAddress) external; function getDelegateExpiry(address _delegateAddress) external view returns (uint256); } ================================================ FILE: contracts/interface/megapool/RocketMegapoolInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import {RocketMegapoolDelegateInterface} from "./RocketMegapoolDelegateInterface.sol"; import {RocketMegapoolProxyInterface} from "./RocketMegapoolProxyInterface.sol"; interface RocketMegapoolInterface is RocketMegapoolDelegateInterface, RocketMegapoolProxyInterface { } ================================================ FILE: contracts/interface/megapool/RocketMegapoolManagerInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import "../../contract/megapool/RocketMegapoolStorageLayout.sol"; import "./RocketMegapoolInterface.sol"; import {BeaconStateVerifierInterface, ValidatorProof, Withdrawal, WithdrawalProof, SlotProof} from "../util/BeaconStateVerifierInterface.sol"; interface RocketMegapoolManagerInterface { struct ExitChallenge { RocketMegapoolDelegateInterface megapool; uint32[] validatorIds; } function getValidatorCount() external view returns (uint256); function addValidator(address _megapoolAddress, uint32 _validatorId, bytes calldata _pubkey) external; function getLastChallenger() external view returns (address); function getValidatorInfo(uint256 _index) external view returns (bytes memory pubkey, RocketMegapoolStorageLayout.ValidatorInfo memory validatorInfo, address megapool, uint32 validatorId); function stake(RocketMegapoolInterface megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _proof, SlotProof calldata _slotProof) external; function dissolve(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _proof, SlotProof calldata _slotProof) external; function notifyExit(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _proof, SlotProof calldata _slotProof) external; function challengeExit(ExitChallenge[] calldata _challenges) external; function notifyNotExit(RocketMegapoolInterface _megapool, uint32 _validatorId, uint64 _slotTimestamp, ValidatorProof calldata _proof, SlotProof calldata _slotProof) external; function notifyFinalBalance(RocketMegapoolInterface megapool, uint32 _validatorId, uint64 _slotTimestamp, WithdrawalProof calldata _proof, ValidatorProof calldata _validatorProof, SlotProof calldata _slotProof) external; } ================================================ FILE: contracts/interface/megapool/RocketMegapoolPenaltiesInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketMegapoolPenaltiesInterface { function getVoteCount(address _megapool, uint256 _block, uint256 _amount) external view returns (uint256); function penalise(address _megapool, uint256 _block, uint256 _amount) external; function executePenalty(address _megapool, uint256 _block, uint256 _amount) external; function getPenaltyRunningTotalAtTime(uint64 _time) external view returns (uint256); function getCurrentPenaltyRunningTotal() external view returns (uint256); function getCurrentMaxPenalty() external view returns (uint256); } ================================================ FILE: contracts/interface/megapool/RocketMegapoolProxyInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import "../../interface/RocketStorageInterface.sol"; interface RocketMegapoolProxyInterface { function initialise(address _nodeAddress) external; function delegateUpgrade() external; function setUseLatestDelegate(bool _state) external; function getUseLatestDelegate() external view returns (bool); function getDelegate() external view returns (address); function getEffectiveDelegate() external view returns (address); function getDelegateExpired() external view returns (bool); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolBaseInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketMinipoolBaseInterface { function initialise(address _rocketStorage, address _nodeAddress) external; function delegateUpgrade() external; function delegateRollback() external; function setUseLatestDelegate(bool _setting) external; function getUseLatestDelegate() external view returns (bool); function getDelegate() external view returns (address); function getPreviousDelegate() external view returns (address); function getEffectiveDelegate() external view returns (address); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolBondReducerInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; interface RocketMinipoolBondReducerInterface { function beginReduceBondAmount(address _minipoolAddress, uint256 _newBondAmount) external; function getReduceBondTime(address _minipoolAddress) external view returns (uint256); function getReduceBondValue(address _minipoolAddress) external view returns (uint256); function getReduceBondCancelled(address _minipoolAddress) external view returns (bool); function canReduceBondAmount(address _minipoolAddress) external view returns (bool); function voteCancelReduction(address _minipoolAddress) external; function reduceBondAmount() external returns (uint256); function getLastBondReductionTime(address _minipoolAddress) external view returns (uint256); function getLastBondReductionPrevValue(address _minipoolAddress) external view returns (uint256); function getLastBondReductionPrevNodeFee(address _minipoolAddress) external view returns (uint256); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolFactoryInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../../types/MinipoolDeposit.sol"; interface RocketMinipoolFactoryInterface { function getExpectedAddress(address _nodeAddress, uint256 _salt) external view returns (address); function deployContract(address _nodeAddress, uint256 _salt) external returns (address); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../../types/MinipoolDeposit.sol"; import "../../types/MinipoolStatus.sol"; import "../RocketStorageInterface.sol"; interface RocketMinipoolInterface { function version() external view returns (uint8); function initialise(address _nodeAddress) external; function getStatus() external view returns (MinipoolStatus); function getFinalised() external view returns (bool); function getStatusBlock() external view returns (uint256); function getStatusTime() external view returns (uint256); function getScrubVoted(address _member) external view returns (bool); function getDepositType() external view returns (MinipoolDeposit); function getNodeAddress() external view returns (address); function getNodeFee() external view returns (uint256); function getNodeDepositBalance() external view returns (uint256); function getNodeRefundBalance() external view returns (uint256); function getNodeDepositAssigned() external view returns (bool); function getPreLaunchValue() external view returns (uint256); function getNodeTopUpValue() external view returns (uint256); function getVacant() external view returns (bool); function getPreMigrationBalance() external view returns (uint256); function getUserDistributed() external view returns (bool); function getUserDepositBalance() external view returns (uint256); function getUserDepositAssigned() external view returns (bool); function getUserDepositAssignedTime() external view returns (uint256); function getTotalScrubVotes() external view returns (uint256); function calculateNodeShare(uint256 _balance) external view returns (uint256); function calculateUserShare(uint256 _balance) external view returns (uint256); function preDeposit(uint256 _bondingValue, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external payable; function deposit() external payable; function userDeposit() external payable; function distributeBalance(bool _rewardsOnly) external; function beginUserDistribute() external; function userDistributeAllowed() external view returns (bool); function refund() external; function slash() external; function finalise() external; function canStake() external view returns (bool); function canPromote() external view returns (bool); function stake(bytes calldata _validatorSignature, bytes32 _depositDataRoot) external; function prepareVacancy(uint256 _bondAmount, uint256 _currentBalance) external; function promote() external; function dissolve() external; function close() external; function voteScrub() external; function reduceBondAmount() external; } ================================================ FILE: contracts/interface/minipool/RocketMinipoolManagerInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; // SPDX-License-Identifier: GPL-3.0-only import "../../types/MinipoolDeposit.sol"; import "../../types/MinipoolDetails.sol"; import "./RocketMinipoolInterface.sol"; interface RocketMinipoolManagerInterface { function getMinipoolCount() external view returns (uint256); function getStakingMinipoolCount() external view returns (uint256); function getFinalisedMinipoolCount() external view returns (uint256); function getActiveMinipoolCount() external view returns (uint256); function getMinipoolRPLSlashed(address _minipoolAddress) external view returns (bool); function getMinipoolCountPerStatus(uint256 offset, uint256 limit) external view returns (uint256, uint256, uint256, uint256, uint256); function getPrelaunchMinipools(uint256 offset, uint256 limit) external view returns (address[] memory); function getMinipoolAt(uint256 _index) external view returns (address); function getNodeMinipoolCount(address _nodeAddress) external view returns (uint256); function getNodeActiveMinipoolCount(address _nodeAddress) external view returns (uint256); function getNodeFinalisedMinipoolCount(address _nodeAddress) external view returns (uint256); function getNodeStakingMinipoolCount(address _nodeAddress) external view returns (uint256); function getNodeStakingMinipoolCountBySize(address _nodeAddress, uint256 _depositSize) external view returns (uint256); function getNodeMinipoolAt(address _nodeAddress, uint256 _index) external view returns (address); function getNodeValidatingMinipoolCount(address _nodeAddress) external view returns (uint256); function getNodeValidatingMinipoolAt(address _nodeAddress, uint256 _index) external view returns (address); function getMinipoolByPubkey(bytes calldata _pubkey) external view returns (address); function getMinipoolExists(address _minipoolAddress) external view returns (bool); function getMinipoolDestroyed(address _minipoolAddress) external view returns (bool); function getMinipoolPubkey(address _minipoolAddress) external view returns (bytes memory); function updateNodeStakingMinipoolCount(uint256 _previousBond, uint256 _newBond, uint256 _previousFee, uint256 _newFee) external; function getMinipoolWithdrawalCredentials(address _minipoolAddress) external pure returns (bytes memory); function createMinipool(address _nodeAddress, uint256 _salt) external returns (RocketMinipoolInterface); function createVacantMinipool(address _nodeAddress, uint256 _salt, bytes calldata _validatorPubkey, uint256 _bondAmount, uint256 _currentBalance) external returns (RocketMinipoolInterface); function removeVacantMinipool() external; function getVacantMinipoolCount() external view returns (uint256); function getVacantMinipoolAt(uint256 _index) external view returns (address); function destroyMinipool() external; function incrementNodeStakingMinipoolCount(address _nodeAddress) external; function decrementNodeStakingMinipoolCount(address _nodeAddress) external; function tryDistribute(address _nodeAddress) external; function incrementNodeFinalisedMinipoolCount(address _nodeAddress) external; function setMinipoolPubkey(bytes calldata _pubkey) external; function getMinipoolDepositType(address _minipoolAddress) external view returns (MinipoolDeposit); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolPenaltyInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketMinipoolPenaltyInterface { // Max penalty rate function setMaxPenaltyRate(uint256 _rate) external; function getMaxPenaltyRate() external view returns (uint256); // Penalty rate function setPenaltyRate(address _minipoolAddress, uint256 _rate) external; function getPenaltyRate(address _minipoolAddress) external view returns(uint256); } ================================================ FILE: contracts/interface/minipool/RocketMinipoolQueueInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../../types/MinipoolDeposit.sol"; interface RocketMinipoolQueueInterface { function getTotalLength() external view returns (uint256); function getContainsLegacy() external view returns (bool); function getLengthLegacy(MinipoolDeposit _depositType) external view returns (uint256); function getLength() external view returns (uint256); function getTotalCapacity() external view returns (uint256); function getEffectiveCapacity() external view returns (uint256); function getNextCapacityLegacy() external view returns (uint256); function getNextDepositLegacy() external view returns (MinipoolDeposit, uint256); function enqueueMinipool(address _minipool) external; function dequeueMinipoolByDepositLegacy(MinipoolDeposit _depositType) external returns (address minipoolAddress); function dequeueMinipools(uint256 _maxToDequeue) external returns (address[] memory minipoolAddress); function removeMinipool(MinipoolDeposit _depositType) external; function getMinipoolAt(uint256 _index) external view returns(address); function getMinipoolPosition(address _minipool) external view returns (int256); } ================================================ FILE: contracts/interface/network/RocketNetworkBalancesInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; // SPDX-License-Identifier: GPL-3.0-only interface RocketNetworkBalancesInterface { function getBalancesTimestamp() external view returns (uint256); function getBalancesBlock() external view returns (uint256); function getTotalETHBalance() external view returns (uint256); function getStakingETHBalance() external view returns (uint256); function getTotalRETHSupply() external view returns (uint256); function getETHUtilizationRate() external view returns (uint256); function submitBalances(uint256 _block, uint256 _slotTimestamp, uint256 _total, uint256 _staking, uint256 _rethSupply) external; function executeUpdateBalances(uint256 _block, uint256 _slotTimestamp, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) external; } ================================================ FILE: contracts/interface/network/RocketNetworkFeesInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketNetworkFeesInterface { function getNodeDemand() external view returns (int256); function getNodeFee() external view returns (uint256); function getNodeFeeByDemand(int256 _nodeDemand) external view returns (uint256); } ================================================ FILE: contracts/interface/network/RocketNetworkPenaltiesInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketNetworkPenaltiesInterface { function getVoteCount(address _minipool, uint256 _block) external view returns (uint256); function submitPenalty(address _minipool, uint256 _block) external; function executeUpdatePenalty(address _minipool, uint256 _block) external; function getPenaltyRunningTotalAtTime(uint64 _time) external view returns (uint256); function getCurrentPenaltyRunningTotal() external view returns (uint256); function getCurrentMaxPenalty() external view returns (uint256); function getPenaltyCount(address _minipoolAddress) external view returns (uint256); } ================================================ FILE: contracts/interface/network/RocketNetworkPricesInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketNetworkPricesInterface { function getPricesBlock() external view returns (uint256); function getRPLPrice() external view returns (uint256); function submitPrices(uint256 _block, uint256 _slotTimestamp, uint256 _rplPrice) external; function executeUpdatePrices(uint256 _block, uint256 _slotTimestamp, uint256 _rplPrice) external; } ================================================ FILE: contracts/interface/network/RocketNetworkRevenuesInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; interface RocketNetworkRevenuesInterface { function initialise(uint256 _initialNodeShare, uint256 _initialVoterShare, uint256 _initialProtocolDAOShare) external; function getCurrentNodeShare() external view returns (uint256); function getCurrentVoterShare() external view returns (uint256); function getCurrentProtocolDAOShare() external view returns (uint256); function setNodeShare(uint256 _newShare) external; function setVoterShare(uint256 _newShare) external; function setProtocolDAOShare(uint256 _newShare) external; function calculateSplit(uint64 _sinceTime) external view returns (uint256 nodeShare, uint256 voterShare, uint256 protocolDAOShare, uint256 rethShare); function setNodeCapitalRatio(address _nodeAddress, uint256 _value) external; function getNodeCapitalRatio(address _nodeAddress) external view returns (uint256); function getNodeAverageCapitalRatioSince(address _nodeAddress, uint64 _sinceTime) external view returns (uint256); } ================================================ FILE: contracts/interface/network/RocketNetworkSnapshotsInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; /// @notice Accounting for snapshotting of values based on block numbers interface RocketNetworkSnapshotsInterface { struct Checkpoint224 { uint32 _block; uint224 _value; } function push(bytes32 _key, uint224 _value) external; function length(bytes32 _key) external view returns (uint256); function latest(bytes32 _key) external view returns (bool exists, uint32 block, uint224 value); function latestBlock(bytes32 _key) external view returns (uint32); function latestValue(bytes32 _key) external view returns (uint224); function lookup(bytes32 _key, uint32 _block) external view returns (uint224); function lookupCheckpoint(bytes32 _key, uint32 _block) external view returns (bool exists, uint32 block, uint224 value); function lookupRecent(bytes32 _key, uint32 _block, uint256 _recency) external view returns (uint224); } ================================================ FILE: contracts/interface/network/RocketNetworkSnapshotsTimeInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; /// @notice Accounting for snapshotting of values based on block timestamps interface RocketNetworkSnapshotsTimeInterface { struct Checkpoint192 { uint64 _time; uint192 _value; } function push(bytes32 _key, uint192 _value) external; function length(bytes32 _key) external view returns (uint256); function latest(bytes32 _key) external view returns (bool exists, uint64 time, uint192 value); function latestTime(bytes32 _key) external view returns (uint64); function latestValue(bytes32 _key) external view returns (uint192); function lookup(bytes32 _key, uint64 _time) external view returns (uint192); function lookupCheckpoint(bytes32 _key, uint64 _time) external view returns (bool exists, uint64 time, uint192 value); function lookupRecent(bytes32 _key, uint64 _time, uint256 _recency) external view returns (uint192); } ================================================ FILE: contracts/interface/network/RocketNetworkVotingInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketNetworkVotingInterface { function getNodeCount(uint32 _block) external view returns (uint256); function getVotingPower(address _nodeAddress, uint32 _block) external view returns (uint256); function setDelegate(address _newDelegate) external; function getDelegate(address _nodeAddress, uint32 _block) external view returns (address); function getCurrentDelegate(address _nodeAddress) external view returns (address); } ================================================ FILE: contracts/interface/node/RocketNodeDepositInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../types/MinipoolDeposit.sol"; interface RocketNodeDepositInterface { struct NodeDeposit { uint256 bondAmount; bool useExpressTicket; bytes validatorPubkey; bytes validatorSignature; bytes32 depositDataRoot; } function getBondRequirement(uint256 _numValidators) external view returns (uint256); function getNodeDepositCredit(address _nodeAddress) external view returns (uint256); function getNodeEthBalance(address _nodeAddress) external view returns (uint256); function getNodeCreditAndBalance(address _nodeAddress) external view returns (uint256); function getNodeUsableCreditAndBalance(address _nodeAddress) external view returns (uint256); function getNodeUsableCredit(address _nodeAddress) external view returns (uint256); function increaseDepositCreditBalance(address _nodeOperator, uint256 _amount) external; function depositEthFor(address _nodeAddress) external payable; function withdrawEth(address _nodeAddress, uint256 _amount) external; function deposit(uint256 _depositAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external payable; function depositWithCredit(uint256 _depositAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external payable; function depositMulti(NodeDeposit[] calldata _deposits) external payable; } ================================================ FILE: contracts/interface/node/RocketNodeDistributorFactoryInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketNodeDistributorFactoryInterface { function getProxyBytecode() external pure returns (bytes memory); function getProxyAddress(address _nodeAddress) external view returns(address); function createProxy(address _nodeAddress) external; } ================================================ FILE: contracts/interface/node/RocketNodeDistributorInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketNodeDistributorInterface { function getNodeShare() external view returns (uint256); function getUserShare() external view returns (uint256); function distribute() external; } ================================================ FILE: contracts/interface/node/RocketNodeManagerInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../types/NodeDetails.sol"; interface RocketNodeManagerInterface { // Structs struct TimezoneCount { string timezone; uint256 count; } function getNodeCount() external view returns (uint256); function getNodeCountPerTimezone(uint256 offset, uint256 limit) external view returns (TimezoneCount[] memory); function getNodeAt(uint256 _index) external view returns (address); function getNodeExists(address _nodeAddress) external view returns (bool); function getNodeWithdrawalAddress(address _nodeAddress) external view returns (address); function getNodePendingWithdrawalAddress(address _nodeAddress) external view returns (address); function getNodeRPLWithdrawalAddress(address _nodeAddress) external view returns (address); function getNodeRPLWithdrawalAddressIsSet(address _nodeAddress) external view returns (bool); function unsetRPLWithdrawalAddress(address _nodeAddress) external; function setRPLWithdrawalAddress(address _nodeAddress, address _newRPLWithdrawalAddress, bool _confirm) external; function confirmRPLWithdrawalAddress(address _nodeAddress) external; function getNodePendingRPLWithdrawalAddress(address _nodeAddress) external view returns (address); function getNodeTimezoneLocation(address _nodeAddress) external view returns (string memory); function registerNode(string calldata _timezoneLocation) external; function getNodeRegistrationTime(address _nodeAddress) external view returns (uint256); function setTimezoneLocation(string calldata _timezoneLocation) external; function setRewardNetwork(address _nodeAddress, uint256 network) external; function getRewardNetwork(address _nodeAddress) external view returns (uint256); function getFeeDistributorInitialised(address _nodeAddress) external view returns (bool); function initialiseFeeDistributor() external; function getAverageNodeFee(address _nodeAddress) external view returns (uint256); function setSmoothingPoolRegistrationState(bool _state) external; function getSmoothingPoolRegistrationState(address _nodeAddress) external returns (bool); function getSmoothingPoolRegistrationChanged(address _nodeAddress) external returns (uint256); function getSmoothingPoolRegisteredNodeCount(uint256 _offset, uint256 _limit) external view returns (uint256); function getNodeAddresses(uint256 _offset, uint256 _limit) external view returns (address[] memory); function deployMegapool() external returns (address); function getExpressTicketCount(address _nodeAddress) external view returns (uint256); function useExpressTicket(address _nodeAddress) external; function provisionExpressTickets(address _nodeAddress) external; function getExpressTicketsProvisioned(address _nodeAddress) external view returns (bool); function refundExpressTicket(address _nodeAddress) external; function getMegapoolAddress(address _nodeAddress) external view returns (address); function getUnclaimedRewards(address _nodeAddress) external view returns (uint256); function addUnclaimedRewards(address _nodeAddress) external payable; function claimUnclaimedRewards(address _nodeAddress) external; } ================================================ FILE: contracts/interface/node/RocketNodeStakingInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; interface RocketNodeStakingInterface { function getTotalStakedRPL() external view returns (uint256); function getTotalMegapoolStakedRPL() external view returns (uint256); function getTotalLegacyStakedRPL() external view returns (uint256); function getNodeStakedRPL(address _nodeAddress) external view returns (uint256); function getNodeMegapoolStakedRPL(address _nodeAddress) external view returns (uint256); function getNodeLegacyStakedRPL(address _nodeAddress) external view returns (uint256); function getNodeUnstakingRPL(address _nodeAddress) external view returns (uint256); function getNodeLockedRPL(address _nodeAddress) external view returns (uint256); function stakeRPLFor(address _nodeAddress, uint256 _amount) external; function stakeRPL(uint256 _amount) external; function unstakeRPL(uint256 _amount) external; function unstakeRPLFor(address _nodeAddress, uint256 _amount) external; function withdrawRPL() external; function withdrawRPLFor(address _nodeAddress) external; function unstakeLegacyRPL(uint256 _amount) external; function unstakeLegacyRPLFor(address _nodeAddress, uint256 _amount) external; function getNodeRPLStakedTime(address _nodeAddress) external view returns (uint256); function getNodeLastUnstakeTime(address _nodeAddress) external view returns (uint256); function setStakeRPLForAllowed(address _caller, bool _allowed) external; function setStakeRPLForAllowed(address _nodeAddress, address _caller, bool _allowed) external; function getRPLLockingAllowed(address _nodeAddress) external view returns (bool); function setRPLLockingAllowed(address _nodeAddress, bool _allowed) external; function getNodeETHBonded(address _nodeAddress) external view returns (uint256); function getNodeMegapoolETHBonded(address _nodeAddress) external view returns (uint256); function getNodeMinipoolETHBonded(address _nodeAddress) external view returns (uint256); function getNodeETHBorrowed(address _nodeAddress) external view returns (uint256); function getNodeMegapoolETHBorrowed(address _nodeAddress) external view returns (uint256); function getNodeMinipoolETHBorrowed(address _nodeAddress) external view returns (uint256); function getNodeMinimumLegacyRPLStake(address _nodeAddress) external view returns (uint256); function getNodeETHCollateralisationRatio(address _nodeAddress) external view returns (uint256); // Internal (not callable by users) function lockRPL(address _nodeAddress, uint256 _amount) external; function unlockRPL(address _nodeAddress, uint256 _amount) external; function transferRPL(address _from, address _to, uint256 _amount) external; function burnRPL(address _from, uint256 _amount) external; function slashRPL(address _nodeAddress, uint256 _ethSlashAmount) external; } ================================================ FILE: contracts/interface/rewards/RocketMerkleDistributorMainnetInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; import "./RocketRewardsRelayInterface.sol"; interface RocketMerkleDistributorMainnetInterface is RocketRewardsRelayInterface { function initialise() external; function claimOutstandingEth() external; function getOutstandingEth(address _address) external view returns (uint256); } ================================================ FILE: contracts/interface/rewards/RocketRewardsPoolInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; import "../../types/RewardSubmission.sol"; // SPDX-License-Identifier: GPL-3.0-only interface RocketRewardsPoolInterface { function getEthBalance() external view returns (uint256); function getRewardIndex() external view returns(uint256); function getRPLBalance() external view returns(uint256); function getPendingRPLRewards() external view returns (uint256); function getPendingETHRewards() external view returns (uint256); function getPendingVoterShare() external view returns (uint256); function getClaimIntervalTimeStart() external view returns(uint256); function getClaimIntervalTime() external view returns(uint256); function getClaimIntervalsPassed() external view returns(uint256); function getClaimIntervalExecutionBlock(uint256 _interval) external view returns(uint256); function getClaimIntervalExecutionAddress(uint256 _interval) external view returns(address); function getClaimingContractPerc(string memory _claimingContract) external view returns(uint256); function getClaimingContractsPerc(string[] memory _claimingContracts) external view returns (uint256[] memory); function getTrustedNodeSubmitted(address _trustedNodeAddress, uint256 _rewardIndex) external view returns (bool); function getSubmissionFromNodeExists(address _trustedNodeAddress, RewardSubmission calldata _submission) external view returns (bool); function getSubmissionCount(RewardSubmission calldata _submission) external view returns (uint256); function submitRewardSnapshot(RewardSubmission calldata _submission) external; function executeRewardSnapshot(RewardSubmission calldata _submission) external; function depositVoterShare() payable external; } ================================================ FILE: contracts/interface/rewards/RocketRewardsRelayInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; struct Claim { uint256 rewardIndex; uint256 amountRPL; uint256 amountSmoothingPoolETH; uint256 amountVoterETH; bytes32[] merkleProof; } interface RocketRewardsRelayInterface { function relayRewards(uint256 _intervalIndex, uint256 _treeVersion, bytes32 _merkleRoot, uint256 _rewardsRPL, uint256 _rewardsETH) external; function claim(address _nodeAddress, Claim[] calldata _claims) external; function claimAndStake(address _nodeAddress, Claim[] calldata _claims, uint256 _stakeAmount) external; function isClaimed(uint256 _intervalIndex, address _claimer) external view returns (bool); } ================================================ FILE: contracts/interface/rewards/RocketSmoothingPoolInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; interface RocketSmoothingPoolInterface { function withdrawEther(address _to, uint256 _amount) external; } ================================================ FILE: contracts/interface/rewards/claims/RocketClaimDAOInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; interface RocketClaimDAOInterface { // Struct for returning data about a payment contract struct PaymentContract { address recipient; uint256 amountPerPeriod; uint256 periodLength; uint256 lastPaymentTime; uint256 numPeriods; uint256 periodsPaid; } function getContractExists(string calldata _contractName) external view returns (bool); function getContract(string calldata _contractName) external view returns (PaymentContract memory); function getBalance(address _recipientAddress) external view returns (uint256); function spend(string memory _invoiceID, address _recipientAddress, uint256 _amount) external; function newContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) external; function updateContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _numPeriods) external; function withdrawBalance(address _recipientAddress) external; function payOutContracts(string[] calldata _contractNames) external; function payOutContractsAndWithdraw(string[] calldata _contractNames) external; } ================================================ FILE: contracts/interface/rewards/claims/RocketClaimNodeInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketClaimNodeInterface { function getEnabled() external view returns (bool); function getClaimPossible(address _nodeAddress) external view returns (bool); function getClaimRewardsPerc(address _nodeAddress) external view returns (uint256); function getClaimRewardsAmount(address _nodeAddress) external view returns (uint256); function register(address _nodeAddress, bool _enable) external; function claim() external; } ================================================ FILE: contracts/interface/rewards/claims/RocketClaimTrustedNodeInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface RocketClaimTrustedNodeInterface { function getEnabled() external view returns (bool); function getClaimPossible(address _trustedNodeAddress) external view returns (bool); function getClaimRewardsPerc(address _trustedNodeAddress) external view returns (uint256); function getClaimRewardsAmount(address _trustedNodeAddress) external view returns (uint256); function register(address _trustedNode, bool _enable) external; function claim() external; } ================================================ FILE: contracts/interface/token/RocketTokenRETHInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../util/IERC20.sol"; interface RocketTokenRETHInterface is IERC20 { function getEthValue(uint256 _rethAmount) external view returns (uint256); function getRethValue(uint256 _ethAmount) external view returns (uint256); function getExchangeRate() external view returns (uint256); function getTotalCollateral() external view returns (uint256); function getCollateralRate() external view returns (uint256); function depositExcess() external payable; function depositExcessCollateral() external; function mint(uint256 _ethAmount, address _to) external; function burn(uint256 _rethAmount) external; } ================================================ FILE: contracts/interface/token/RocketTokenRPLInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "../util/IERC20.sol"; interface RocketTokenRPLInterface is IERC20 { function getInflationCalcTime() external view returns(uint256); function getInflationIntervalTime() external view returns(uint256); function getInflationIntervalRate() external view returns(uint256); function getInflationIntervalsPassed() external view returns(uint256); function getInflationIntervalStartTime() external view returns(uint256); function getInflationRewardsContractAddress() external view returns(address); function inflationCalculate() external view returns (uint256); function inflationMintTokens() external returns (uint256); function swapTokens(uint256 _amount) external; } ================================================ FILE: contracts/interface/util/AddressQueueStorageInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface AddressQueueStorageInterface { function getLength(bytes32 _key) external view returns (uint); function getItem(bytes32 _key, uint _index) external view returns (address); function getIndexOf(bytes32 _key, address _value) external view returns (int); function enqueueItem(bytes32 _key, address _value) external; function dequeueItem(bytes32 _key) external returns (address); function removeItem(bytes32 _key, address _value) external; } ================================================ FILE: contracts/interface/util/AddressSetStorageInterface.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only interface AddressSetStorageInterface { function getCount(bytes32 _key) external view returns (uint); function getItem(bytes32 _key, uint _index) external view returns (address); function getIndexOf(bytes32 _key, address _value) external view returns (int); function addItem(bytes32 _key, address _value) external; function removeItem(bytes32 _key, address _value) external; } ================================================ FILE: contracts/interface/util/BeaconStateVerifierInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; struct Withdrawal { uint64 index; uint64 validatorIndex; bytes20 withdrawalCredentials; uint64 amountInGwei; } struct WithdrawalProof { uint64 withdrawalSlot; uint16 withdrawalNum; Withdrawal withdrawal; bytes32[] witnesses; } struct Validator { bytes pubkey; bytes32 withdrawalCredentials; uint64 effectiveBalance; bool slashed; uint64 activationEligibilityEpoch; uint64 activationEpoch; uint64 exitEpoch; uint64 withdrawableEpoch; } struct ValidatorProof { uint40 validatorIndex; Validator validator; bytes32[] witnesses; } struct SlotProof { uint64 slot; bytes32[] witnesses; } interface BeaconStateVerifierInterface { function verifyValidator(uint64 _slotTimestamp, uint64 _slot, ValidatorProof calldata _proof) external view returns (bool); function verifyWithdrawal(uint64 _slotTimestamp, uint64 _slot, WithdrawalProof calldata _proof) external view returns (bool); function verifySlot(uint64 _slotTimestamp, SlotProof calldata _proof) external view returns (bool); } ================================================ FILE: contracts/interface/util/IERC20.sol ================================================ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) pragma solidity >0.5.0 <0.9.0; /** * @dev Interface of the ERC20 standard as defined in the EIP. */ interface IERC20 { /** * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ event Approval(address indexed owner, address indexed spender, uint256 value); /** * @dev Returns the amount of tokens in existence. */ function totalSupply() external view returns (uint256); /** * @dev Returns the amount of tokens owned by `account`. */ function balanceOf(address account) external view returns (uint256); /** * @dev Moves `amount` tokens from the caller's account to `to`. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transfer(address to, uint256 amount) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */ function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. * * Returns a boolean value indicating whether the operation succeeded. * * IMPORTANT: Beware that changing an allowance with this method brings the risk * that someone may use both the old and the new allowance by unfortunate * transaction ordering. One possible solution to mitigate this race * condition is to first reduce the spender's allowance to 0 and set the * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * * Emits an {Approval} event. */ function approve(address spender, uint256 amount) external returns (bool); /** * @dev Moves `amount` tokens from `from` to `to` using the * allowance mechanism. `amount` is then deducted from the caller's * allowance. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 amount) external returns (bool); } ================================================ FILE: contracts/interface/util/IERC20Burnable.sol ================================================ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) import "./IERC20.sol"; pragma solidity >0.5.0 <0.9.0; interface IERC20Burnable is IERC20 { function burn(uint256 amount) external; function burnFrom(address account, uint256 amount) external; } ================================================ FILE: contracts/interface/util/LinkedListStorageInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; pragma abicoder v2; interface LinkedListStorageInterface { struct DepositQueueValue { address receiver; // the address that will receive the requested value uint32 validatorId; // internal validator id uint32 suppliedValue; // in milliether uint32 requestedValue; // in milliether } struct DepositQueueKey { address receiver; // the address that will receive the requested value uint32 validatorId; // internal validator id } function getLength(bytes32 _namespace) external view returns (uint256); function getItem(bytes32 _namespace, uint256 _index) external view returns (DepositQueueValue memory); function peekItem(bytes32 _namespace) external view returns (DepositQueueValue memory); function getIndexOf(bytes32 _namespace, DepositQueueKey memory _key) external view returns (uint256); function getHeadIndex(bytes32 _namespace) external view returns (uint256); function enqueueItem(bytes32 _namespace, DepositQueueValue memory _value) external; function dequeueItem(bytes32 _namespace) external returns (DepositQueueValue memory); function removeItem(bytes32 _namespace, DepositQueueKey memory _key) external; function scan(bytes32 _namespace, uint256 _startIndex, uint256 _count) external view returns (DepositQueueValue[] memory entries, uint256 nextIndex); } ================================================ FILE: contracts/thirdparty/EthBalanceChecker/EthBalanceChecker.sol ================================================ // https://github.com/wbobeirne/eth-balance-checker/blob/master/contracts/BalanceChecker.sol // Built off of https://github.com/DeltaBalances/DeltaBalances.github.io/blob/master/smart_contract/deltabalances.sol pragma solidity 0.8.30; // ERC20 contract interface interface Token { function balanceOf(address) external view returns (uint); } contract EthBalanceChecker { /* Fallback function, don't accept any ETH */ fallback() external payable { revert("BalanceChecker does not accept payments"); } /* Check the token balance of a wallet in a token contract Returns the balance of the token for user. Avoids possible errors: - return 0 on non-contract address - returns 0 if the contract doesn't implement balanceOf */ function tokenBalance(address user, address token) public view returns (uint) { // check if token is actually a contract uint256 tokenCode; assembly { tokenCode := extcodesize(token) } // contract code size // is it a contract and does it implement balanceOf if (tokenCode > 0) { (bool success, bytes memory result) = token.staticcall(abi.encodeWithSignature("balanceOf(address)", user)); if (!success) { return 0; } (uint256 balance) = abi.decode(result, (uint256)); return balance; } return 0; } /* Check the token balances of a wallet for multiple tokens. Pass 0x0 as a "token" address to get ETH balance. Possible error throws: - extremely large arrays for user and or tokens (gas cost too high) Returns a one-dimensional that's user.length * tokens.length long. The array is ordered by all of the 0th users token balances, then the 1th user, and so on. */ function balances(address[] calldata users, address[] calldata tokens) external view returns (uint[] memory) { uint[] memory addrBalances = new uint[](tokens.length * users.length); for(uint i = 0; i < users.length; i++) { for (uint j = 0; j < tokens.length; j++) { uint addrIdx = j + tokens.length * i; if (tokens[j] != address(0x0)) { addrBalances[addrIdx] = tokenBalance(users[i], tokens[j]); } else { addrBalances[addrIdx] = users[i].balance; // ETH balance } } } return addrBalances; } } ================================================ FILE: contracts/thirdparty/Multicall2/Multicall2.sol ================================================ // https://github.com/sky-ecosystem/multicall/blob/master/src/Multicall2.sol pragma solidity 0.8.30; /// @title Multicall2 - Aggregate results from multiple read-only function calls /// @author Michael Elliot /// @author Joshua Levine /// @author Nick Johnson contract Multicall2 { struct Call { address target; bytes callData; } struct Result { bool success; bytes returnData; } function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { blockNumber = block.number; returnData = new bytes[](calls.length); for(uint256 i = 0; i < calls.length; i++) { (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); require(success, "Multicall aggregate: call failed"); returnData[i] = ret; } } function blockAndAggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); } function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { blockHash = blockhash(blockNumber); } function getBlockNumber() public view returns (uint256 blockNumber) { blockNumber = block.number; } function getCurrentBlockCoinbase() public view returns (address coinbase) { coinbase = block.coinbase; } function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { difficulty = block.difficulty; } function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { gaslimit = block.gaslimit; } function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { timestamp = block.timestamp; } function getEthBalance(address addr) public view returns (uint256 balance) { balance = addr.balance; } function getLastBlockHash() public view returns (bytes32 blockHash) { blockHash = blockhash(block.number - 1); } function tryAggregate(bool requireSuccess, Call[] memory calls) public returns (Result[] memory returnData) { returnData = new Result[](calls.length); for(uint256 i = 0; i < calls.length; i++) { (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); if (requireSuccess) { require(success, "Multicall2 aggregate: call failed"); } returnData[i] = Result(success, ret); } } function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { blockNumber = block.number; blockHash = blockhash(block.number); returnData = tryAggregate(requireSuccess, calls); } } ================================================ FILE: contracts/thirdparty/RocketSignerRegistry/RocketSignerRegistry.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.30; import {Strings} from "@openzeppelin4/contracts/utils/Strings.sol"; import {RocketSignerRegistryInterface} from "./interface/RocketSignerRegistryInterface.sol"; /// @notice Maintains a one-to-one forward and reverse mapping of addresses to their delegated signer contract RocketSignerRegistry is RocketSignerRegistryInterface { mapping(address => address) public nodeToSigner; mapping(address => address) public signerToNode; event SignerSet(address indexed nodeAddress, address signerAddress); /// @notice Sets a signing delegate for the caller /// @param _signer The address to which off-chain voting is delegated to for the caller /// @param _v v component of signature giving node permission to use the given signer /// @param _r r component of signature giving node permission to use the given signer /// @param _s s component of signature giving node permission to use the given signer function setSigner(address _signer, uint8 _v, bytes32 _r, bytes32 _s) external { require(msg.sender != _signer, "Cannot set to self"); require(signerToNode[_signer] == address(0), "Signer address already in use"); require(_signer != address(0), "Invalid signer"); require(recoverSigner(msg.sender, _v, _r, _s) == _signer, "Invalid signature"); // Clear existing reverse mapping address previousSigner = nodeToSigner[msg.sender]; if (previousSigner != address(0)) { delete signerToNode[previousSigner]; } // Store new mapping signerToNode[_signer] = msg.sender; nodeToSigner[msg.sender] = _signer; // Emit event emit SignerSet(msg.sender, _signer); } /// @notice Clears the signing delegate for the caller function clearSigner() external { // Clear existing reverse mapping address previousSigner = nodeToSigner[msg.sender]; require (previousSigner != address(0), "No signer set"); // Clear mappings delete signerToNode[previousSigner]; delete nodeToSigner[msg.sender]; // Emit event emit SignerSet(msg.sender, address(0)); } /// @dev Recovers the address which signed a payload including the given node's address function recoverSigner(address _node, uint8 _v, bytes32 _r, bytes32 _s) internal pure returns(address) { bytes memory message = abi.encodePacked(Strings.toHexString(_node), " may delegate to me for Rocket Pool governance"); bytes memory prefixedMessage = abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(message.length), message); bytes32 prefixedHash = keccak256(prefixedMessage); return ecrecover(prefixedHash, _v, _r, _s); } } ================================================ FILE: contracts/thirdparty/RocketSignerRegistry/interface/RocketSignerRegistryInterface.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.13; interface RocketSignerRegistryInterface { function nodeToSigner(address) external view returns (address); function signerToNode(address) external view returns (address); function setSigner(address _signer, uint8 _v, bytes32 _r, bytes32 _s) external; function clearSigner() external; } ================================================ FILE: contracts/thirdparty/UniswapOracleMock/UniswapOracleMock.sol ================================================ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.30; /** @dev Mock contract to return some valid values for RPL Uniswap TWAP oracle */ contract UniswapOracleMock { function observe(uint32[] calldata) external pure returns (int56[] memory, uint160[] memory) { int56[] memory tickCumulatives = new int56[](1); tickCumulatives[0] = 6108781823772; uint160[] memory secondsPerLiquidityCumulativeX128s = new uint160[](1); secondsPerLiquidityCumulativeX128s[0] = 356275638166222587133100043722184061953921; return (tickCumulatives, secondsPerLiquidityCumulativeX128s); } } ================================================ FILE: contracts/types/MinipoolDeposit.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only // Represents the type of deposits required by a minipool enum MinipoolDeposit { None, // Marks an invalid deposit type Full, // The minipool requires 32 ETH from the node operator, 16 ETH of which will be refinanced from user deposits Half, // The minipool required 16 ETH from the node operator to be matched with 16 ETH from user deposits Empty, // The minipool requires 0 ETH from the node operator to be matched with 32 ETH from user deposits (trusted nodes only) Variable // Indicates this minipool is of the new generation that supports a variable deposit amount } ================================================ FILE: contracts/types/MinipoolDetails.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only import "./MinipoolDeposit.sol"; import "./MinipoolStatus.sol"; // A struct containing all the information on-chain about a specific minipool struct MinipoolDetails { bool exists; address minipoolAddress; bytes pubkey; MinipoolStatus status; uint256 statusBlock; uint256 statusTime; bool finalised; MinipoolDeposit depositType; uint256 nodeFee; uint256 nodeDepositBalance; bool nodeDepositAssigned; uint256 userDepositBalance; bool userDepositAssigned; uint256 userDepositAssignedTime; bool useLatestDelegate; address delegate; address previousDelegate; address effectiveDelegate; uint256 penaltyCount; uint256 penaltyRate; address nodeAddress; } ================================================ FILE: contracts/types/MinipoolStatus.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only // Represents a minipool's status within the network enum MinipoolStatus { Initialised, // The minipool has been initialised and is awaiting a deposit of user ETH Prelaunch, // The minipool has enough ETH to begin staking and is awaiting launch by the node operator Staking, // The minipool is currently staking Withdrawable, // NO LONGER USED Dissolved // The minipool has been dissolved and its user deposited ETH has been returned to the deposit pool } ================================================ FILE: contracts/types/NodeDetails.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only // A struct containing all the information on-chain about a specific node struct NodeDetails { bool exists; uint256 registrationTime; string timezoneLocation; bool feeDistributorInitialised; address feeDistributorAddress; uint256 rewardNetwork; uint256 rplStake; uint256 effectiveRPLStake; uint256 minimumRPLStake; uint256 maximumRPLStake; uint256 ethMatched; uint256 ethMatchedLimit; uint256 minipoolCount; uint256 balanceETH; uint256 balanceRETH; uint256 balanceRPL; uint256 balanceOldRPL; uint256 depositCreditBalance; uint256 distributorBalanceUserETH; uint256 distributorBalanceNodeETH; address withdrawalAddress; address pendingWithdrawalAddress; bool smoothingPoolRegistrationState; uint256 smoothingPoolRegistrationChanged; address nodeAddress; } ================================================ FILE: contracts/types/RewardSubmission.sol ================================================ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >0.5.0 <0.9.0; struct RewardSubmission { uint256 rewardIndex; // Index of the reward submission uint256 executionBlock; // Execution block at which the calculations were made uint256 consensusBlock; // Consensus block containing the execution block bytes32 merkleRoot; // Merkle root of the reward claims uint256 intervalsPassed; // Number of intervals passed since last submission (usually 1) uint256 smoothingPoolETH; // Balance to extract from smoothing pool to fulfil this reward interval uint256 treasuryRPL; // How much RPL is to be sent to the pDAO treasury uint256 treasuryETH; // Amount of ETH to send to pDAO treasury uint256 userETH; // Amount to send to rETH uint256[] trustedNodeRPL; // Mapping of RPL rewards for oDAO members on each network uint256[] nodeRPL; // Mapping of RPL rewards for nodes on each network uint256[] nodeETH; // Mapping of ETH rewards for nodes on each network } ================================================ FILE: contracts/types/SettingType.sol ================================================ pragma solidity >0.5.0 <0.9.0; // SPDX-License-Identifier: GPL-3.0-only enum SettingType { UINT256, BOOL, ADDRESS, STRING, BYTES, BYTES32, INT256 } ================================================ FILE: hardhat-common.config.js ================================================ require('dotenv').config(); require('@nomicfoundation/hardhat-ethers'); // Importing babel to be able to use ES6 imports require('@babel/register')({ presets: [ ['@babel/preset-env', { 'targets': { 'node': '16', }, }], ], only: [/test|scripts/], retainLines: true, }); require('@babel/polyfill'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { compilers: [ { version: '0.7.6', settings: { optimizer: { enabled: false, }, }, }, { version: '0.8.30', settings: { optimizer: { enabled: false, }, }, }, ], }, networks: {}, paths: { sources: './contracts', tests: './test', cache: './cache', artifacts: './artifacts', }, mocha: { timeout: 0, }, }; ================================================ FILE: hardhat-deploy.config.js ================================================ require('hardhat-gas-reporter'); require('solidity-coverage'); require('hardhat-ignore-warnings'); let common = require('./hardhat-common.config.js'); // Config from environment const mnemonicPhrase = process.env.MNEMONIC || 'test test test test test test test test test test test junk'; const mnemonicPassword = process.env.MNEMONIC_PASSWORD; const providerUrl = process.env.PROVIDER_URL || 'http://localhost:8545'; module.exports = Object.assign(common, { solidity: { compilers: [ { version: '0.8.30', settings: { viaIR: true, optimizer: { enabled: true, runs: 15000, }, }, }, { version: '0.7.6', settings: { optimizer: { enabled: true, runs: 15000, }, }, }, ], }, networks: { custom: { url: `${providerUrl}`, accounts: { mnemonic: mnemonicPhrase, path: 'm/44\'/60\'/0\'/0', initialIndex: 0, count: 1, passphrase: mnemonicPassword, }, network_id: '*', }, }, gasReporter: { enabled: !!process.env.REPORT_GAS, }, warnings: { '@openzeppelin/**/*': { default: 'off' }, '*': { 'func-mutability': 'off', 'unused-param': 'off', } } }); ================================================ FILE: hardhat.config.js ================================================ require('hardhat-gas-reporter'); require('solidity-coverage'); require('hardhat-ignore-warnings'); let common = require('./hardhat-common.config.js'); // Config from environment const mnemonicPhrase = process.env.MNEMONIC || 'test test test test test test test test test test test junk'; const mnemonicPassword = process.env.MNEMONIC_PASSWORD; const providerUrl = process.env.PROVIDER_URL || 'http://localhost:8545'; module.exports = Object.assign(common, { networks: { hardhat: { allowUnlimitedContractSize: true, accounts: { count: 50, accountsBalance: '10000000000000000000000000', }, }, localhost: { host: '127.0.0.1', port: 8545, network_id: '*', }, custom: { url: `${providerUrl}`, accounts: { mnemonic: mnemonicPhrase, path: 'm/44\'/60\'/0\'/0', initialIndex: 0, count: 1, passphrase: mnemonicPassword, }, network_id: '*', }, }, gasReporter: { enabled: !!process.env.REPORT_GAS, }, warnings: { '@openzeppelin/**/*': { default: 'off' }, '*': { 'func-mutability': 'off', 'unused-param': 'off', } } }); ================================================ FILE: package.json ================================================ { "name": "rocketpool", "version": "3.0.0", "description": "A next generation decentralised Ethereum 2.0 proof of stake protocol", "repository": "git@github.com:rocket-pool/rocketpool.git", "author": "David Rugendyke ", "scripts": { "compile": "hardhat compile", "deploy": "hardhat run scripts/deploy.js", "test": "hardhat test --bail", "test-deploy": "hardhat test --config hardhat-deploy.config.js --bail", "test-gas": "REPORT_GAS=1 hardhat test --config hardhat-deploy.config.js --bail", "test-fork": "hardhat test --config hardhat-fork.config.js --bail", "test-upgrade": "./scripts/upgrade-test.sh", "coverage": "SOLIDITY_COVERAGE=true hardhat coverage", "slither": "slither --filter-paths node_modules,old --exclude conformance-to-solidity-naming-conventions ." }, "devDependencies": { "@babel/core": "^7.25.8", "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.20.2", "@babel/register": "^7.18.9", "@babel/runtime": "^7.20.1", "@chainsafe/lodestar-types": "^0.5.0", "@chainsafe/ssz": "^0.6.1", "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-network-helpers": "^1.0.12", "@openzeppelin/contracts": "^3.4.0", "@openzeppelin4/contracts": "npm:@openzeppelin/contracts@^4.9.2", "axios": "1.8.2", "dotenv": "^16.0.3", "ethers": "^6.13.3", "hardhat": "2.22.12", "hardhat-gas-reporter": "^2.3.0", "mocha": "^10.1.0", "pako": "^1.0.6", "solidity-coverage": "^0.8.13" }, "dependencies": { "hardhat-ignore-warnings": "^0.2.12" } } ================================================ FILE: remapping.json ================================================ { "remappings": [ "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", "@openzeppelin4/contracts/=node_modules/@openzeppelin4/contracts/" ] } ================================================ FILE: scripts/console.js ================================================ import { artifacts, RocketDAONodeTrusted, RocketDAONodeTrustedSettingsProposals, RocketDAONodeTrustedUpgrade, RocketDAOProtocol, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsProposals, RocketDepositPool, RocketMinipoolDelegate, RocketNodeDistributorFactory, RocketRewardsPool, } from '../test/_utils/artifacts.js'; import pako from 'pako'; // import { getValidatorInfo } from '../test/_helpers/megapool'; import fs from 'fs'; import { injectBNHelpers } from '../test/_helpers/bn'; import { Interface } from 'ethers'; const ethers = hre.ethers; injectBNHelpers(); // const rocketStorageAddress = '0xF1ab701bDbc5e3628e97d5416aA8BCc1eB4838c1'; // Scratchnet-4 // const rocketStorageAddress = '0x8a7FB51dAdF638058fBB3f7357c6b5dFbCd2687C'; // Devnet // const rocketStorageAddress = '0xb5E573454086c1ddbd66F27DCCE29426D7689ECC'; // Devnet-3v2 const rocketStorageAddress = '0x594Fb75D3dc2DFa0150Ad03F99F97817747dd4E1'; // Testnet // const rocketStorageAddress = '0xf4D539F1babDAa6E47b1112Bc9Fa1C83cF0FfE59'; // Devnet-4 // const rocketStorageAddress = '0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46'; function compressABI(abi) { return Buffer.from(pako.deflate(JSON.stringify(abi))).toString('base64'); } export function decompressABI(abi) { return JSON.parse(pako.inflate(Buffer.from(abi, 'base64'), { to: 'string' })); } function loadABI(abiFilePath) { return JSON.parse(fs.readFileSync(abiFilePath)); } const startOffset = 256n - 64n; const endOffset = 256n - 128n; const lengthOffset = 256n - 192n; const uint64Mask = BigInt('0xffffffffffffffff'); // const linkedListStorage = LinkedListStorage.at('0x15c13FA4C2FFBAeeF804CB58fFE215aD91732591'); export async function findInQueue(megapoolAddress, validatorId, queueKey, indexOffset = 0n, positionOffset = 0n) { const maxSliceLength = 100n; // Number of entries to scan per call validatorId = BigInt(validatorId); // const linkedListStorage = await LinkedListStorage.deployed(); const scan = await linkedListStorage.scan(ethers.solidityPackedKeccak256(['string'], [queueKey]), indexOffset, maxSliceLength); for (const entry of scan[0]) { if (entry[0].toLowerCase() === megapoolAddress.toLowerCase()) { if (entry[1] === validatorId) { // Found the entry return positionOffset; } } positionOffset += 1n; } if (scan[1] === 0n) { // We hit the end of the queue without finding the entry return null; } else { // Nothing in this slice, recurse until end of queue is reached return await findInQueue(megapoolAddress, validatorId, queueKey, scan[1], positionOffset); } } export async function calculatePositionInQueue(megapool, validatorId) { const { expressUsed } = await getValidatorInfo(megapool, validatorId); const queueKeyString = expressUsed ? 'deposit.queue.express' : 'deposit.queue.standard'; const position = await findInQueue(megapool.target, validatorId, queueKeyString); if (position === null) { // Not found in the queue return null; } // const linkedListStorage = await LinkedListStorage.deployed(); const rocketDepositPool = await RocketDepositPool.deployed(); const rocketDAOProtocolSettingsDeposit = await RocketDAOProtocolSettingsDeposit.deployed(); const expressQueueLength = await linkedListStorage.getLength(ethers.solidityPackedKeccak256(['string'], ['deposit.queue.express'])); const standardQueueLength = await linkedListStorage.getLength(ethers.solidityPackedKeccak256(['string'], ['deposit.queue.standard'])); const queueIndex = await rocketDepositPool.getQueueIndex(); const expressQueueRate = await rocketDAOProtocolSettingsDeposit.getExpressQueueRate(); const queueInterval = expressQueueRate + 1n; if (expressUsed) { let standardEntriesBefore = (position + (queueIndex % queueInterval)) / expressQueueRate; if (standardEntriesBefore > standardQueueLength) { standardEntriesBefore = standardQueueLength; } return position + standardEntriesBefore; } else { let expressEntriesBefore = (position * expressQueueLength) + (expressQueueRate - (queueIndex % queueInterval)); if (expressEntriesBefore > expressQueueLength) { expressEntriesBefore = expressQueueLength; } return position + expressEntriesBefore; } } async function bootstrapUpgrade(type, name, abi, target, { from }) { // const rocketDAONodeTrustedUpgrade = await RocketDAONodeTrustedUpgrade.deployed(); const rocketDAONodeTrustedUpgrade = RocketDAONodeTrustedUpgrade.at('0xE1F9E44d8Fb154c0eF86C826918BF3186eEf1AE9'); await (await rocketDAONodeTrustedUpgrade.connect(from).bootstrapUpgrade(type, name, abi, target)).wait(); } export async function go() { const [guardian] = await ethers.getSigners(); await artifacts.loadFromDeployment(rocketStorageAddress); const rocketDAONodeTrusted = (await RocketDAONodeTrusted.deployed()).connect(guardian); const rocketDAONodeTrustedSettingsProposals = await RocketDAONodeTrustedSettingsProposals.deployed() const currentVoteDelayTime = await rocketDAONodeTrustedSettingsProposals.getVoteDelayTime() console.log(`Current value: ${currentVoteDelayTime}`) await rocketDAONodeTrusted.connect(guardian).bootstrapSettingUint('rocketDAONodeTrustedSettingsProposals', 'proposal.cooldown.time', 60); } function old() { // const delegateAbi = artifacts.require('RocketMinipoolDelegate').abi; // const proxyAbi = artifacts.require('RocketMinipoolBase').abi; // const rocketMegapoolAbi = [...delegateAbi, ...proxyAbi].filter(fragment => fragment.type !== 'constructor'); // console.log(JSON.stringify(rocketMegapoolAbi, null, 2)); // console.log(compressABI(rocketMegapoolAbi)); // const StorageHelper = artifacts.require('StorageHelper'); // const storageHelperAddress = '0x3293eB907B81310EaE55f6bc83393D9632C09665'; // const storageHelper = artifacts.require('StorageHelper').at(storageHelperAddress); // // const tx = storageHelper.setUint(ethers.solidityPackedKeccak256(['string'], ['rewards.pool.claim.interval.time.start']), 1764511104); // console.log(tx); // const iface = new Interface(StorageHelper.abi); // console.log(iface.encodeFunctionData('setUint', [ // ethers.solidityPackedKeccak256(['string'], ['rewards.pool.claim.interval.time.start']), // 1764511104, // ])); // const rocketRewardsPoolAddress = '0x9f94efd898aA8612A1D45C6afc020442B553488B'; // Emphemery // const iface = new Interface(RocketRewardsPool.abi) // const result = iface.decodeFunctionData('submitRewardSnapshot', '0x5d3e8ffa0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000103b900000000000000000000000000000000000000000000000000000000000139dfb2b11d89ce98307ced6e620dacef266164e672ff13014c22bdcf46864ed8405d0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000343bab28172cbb6d3000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000023e905ab8feec0db00000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000005b686b86288e47fee800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000') // console.log(result); // const rocketRewardsPool = RocketRewardsPool.at(rocketRewardsPoolAddress); // console.log(await rocketRewardsPool.getClaimIntervalTimeStart()); // console.log(await rocketRewardsPool.getClaimIntervalsPassed()); // const rocketTokenRPLAddress = '0x594bA8cBbB5fc650BaB6227F06c62FEa1c8D7aaB'; // Emphemery // const rocketTokenRPL = RocketTokenRPL.at(rocketTokenRPLAddress); // console.log(await rocketTokenRPL.getInflationIntervalStartTime()); // const rocketVaultAddress = '0x18E2C4eE58e830166258177BE18f8d09b47Ba8d1' // const rocketVault = RocketVault.at(rocketVaultAddress); // console.log(await rocketVault.balanceOfToken("rocketRewardsPool", rocketTokenRPLAddress)) // console.log(JSON.stringify(RocketRewardsPool.abi)); } export async function stuff() { // const [guardian] = await ethers.getSigners(); // await artifacts.loadFromDeployment(rocketStorageAddress); // const rocketMinipoolManager = await RocketMinipoolManager.deployed(); // const rocketNodeManager = await RocketNodeManager.deployed(); // const rocketStorage = RocketStorage.at(rocketStorageAddress); // // const nodeCount = Number(await rocketNodeManager.getNodeCount()); // const rocketMinipoolManager = await RocketMinipoolManager.at('0xF82991Bd8976c243eB3b7CDDc52AB0Fc8dc1246C'); // const rocketNodeStaking = await RocketNodeStaking.at('0xF18Dc176C10Ff6D8b5A17974126D43301F8EEB95'); // const rocketNetworkSnapshots = await RocketNetworkSnapshots.at('0x7603352f1C4752Ac07AAC94e48632b65FDF1D35c'); // const rocketStorage = await RocketStorage.at('0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46'); // 0xC0ffEE16Ee76bF4b6423437e0C05fBF4421AaDfA // const affectedMinipools = [ // '0xCFa536870E9c62279d29e23D3BED0A8A2380B49E', // '0xA379dCcbc3fFD7230C19B9823c3974a264a44Ae2', // '0x51c6451f8C236abace363E07d2e95002C1F19261', // '0x393e10e682bbbf8fcd51FdC4A60df7B8f4db7f32', // '0xCFD0b06CD51F03e565FE593685537B45DcAD4d4E', // '0xa0c6f60a09A512cD2F259D44Ee049F114475fA5f', // '0xdd5255605830a3A5c698d6b1C1134A65359B65C1', // '0xFD827FF7A8FE439A92D7389B8D78C194e310424a', // '0xce8a6440e8BBfEE059469c6ffE107aA571902956', // '0xc402f5C68B79544D6EC71757E5292ecCFc2b29A7', // '0x3B29C7a245A668159c9528046D56364e84e8Aecc', // '0x8d1094464e0e276DD74fa6F97A074A62211fa254', // '0xBEB6F2CfCF3655453E927Aa6Ef62d7233419064e', // '0x15D5c0d1bE3d134CDAC4c458CBF04F716c4Ab899', // '0x911977960e3655f78BdB6a481ca407cb03Bd488d', // ]; // // for (const minipoolAddress of affectedMinipools) { // const minipool = await RocketMinipoolDelegate.at(minipoolAddress); // const node = await minipool.getNodeAddress(); // const ratio = await rocketNodeStaking.getNodeETHCollateralisationRatio(node); // const provided = await rocketNodeStaking.getNodeETHProvided(node); // const matched = await rocketNodeStaking.getNodeETHMatched(node); // const minipoolCount = await rocketMinipoolManager.getNodeActiveMinipoolCount(node); // // console.log(`${minipoolAddress} ${node} ${provided} ${matched} ${ratio} ${minipoolCount}`); // } // const node = '0xca317A4ecCbe0Dd5832dE2A7407e3c03F88b2CdD'; // // const blocks = // [ // 20149100n, // 20149118n, // 20995895n, // 21002092n, // 21039660n, // 21039731n, // 21051639n, // 21083353n, // 21083357n, // 21083361n, // ] // // let current = 0n; // // const matchedKey = ethers.solidityPackedKeccak256(['string', 'address'], ["eth.matched.node.amount", node]); // const activeKey = ethers.solidityPackedKeccak256(['string', 'address'], ["minipools.active.count", node]); // // const legacy = await rocketStorage.getUint(matchedKey); // console.log(`Legacy: ${legacy}`); // // for (const block of blocks) { // const matchedValue = await rocketNetworkSnapshots.lookup(matchedKey, block); // const activeValue = await rocketNetworkSnapshots.lookup(activeKey, block); // const diff = current - matchedValue; // console.log(`${block} - ${activeValue} ${matchedValue} ${diff}`); // current = matchedValue; // } // // // process.exit(0); // // const node = '0xbE016160562388d912B7Fb98AED5a4dF61d75Df8'; // const count = await rocketMinipoolManager.getNodeMinipoolCount(node); // // let totalNodeBalance = 0n; // let totalUserBalance = 0n; // // let count16 = 0n; // let count8 = 0n; // // for (let i = 0n; i < count; i++) { // const minipoolAddress = await rocketMinipoolManager.getNodeMinipoolAt(node, i); // const minipool = await RocketMinipoolDelegate.at(minipoolAddress); // // const status = await minipool.getStatus(); // // const userBalance = await minipool.getUserDepositBalance(); // const nodeBalance = await minipool.getNodeDepositBalance(); // // console.log(`#${i},${status},${minipoolAddress},${nodeBalance},${userBalance}`); // // if (nodeBalance === 16000000000000000000n) { // count16++; // } else { // count8++; // } // // totalNodeBalance += nodeBalance; // totalUserBalance += userBalance; // } // // console.log(`Count 16: ${count16}`); // console.log(`Count 8: ${count8}`); // // console.log(`User: ${totalUserBalance}`); // console.log(`Node: ${totalNodeBalance}`); // // const provided = await rocketNodeStaking.getNodeETHProvided(node); // const matched = await rocketNodeStaking.getNodeETHMatched(node); // // console.log(`Provided: ${provided}`); // console.log(`Matched: ${matched}`); // const vacantCount = await rocketMinipoolManager.getVacantMinipoolCount(); // console.log(`Vacant minipool count: ${vacantCount}`); // const count = await rocketMinipoolManager.getMinipoolCount(); // console.log(`Minipool count: ${count}`); // // for (let i = 0n; i < count; i++) { // const minipoolAddress = await rocketMinipoolManager.getMinipoolAt(i); // const minipool = await RocketMinipoolDelegate.at(minipoolAddress); // const status = await minipool.getStatus(); // // if (status !== 2n) { // console.log(`#${i} ${minipoolAddress} - ${status}`); // } // // if (i % 1000n === 0n) { // console.log(`${i} of ${count} `); // } // } let notInit = 0; const rocketNodeDistributorFactory = await RocketNodeDistributorFactory.deployed(); for (let i = 0; i < nodeCount; i++) { // const node = '0x5df9a9d48314eE84b3963f1BC9bc812B595AE409'; const node = await rocketNodeManager.getNodeAt(i); const averageNodeFee = await rocketNodeManager.getAverageNodeFee(node); const initialised = await rocketNodeManager.getFeeDistributorInitialised(node); if (!initialised) { // const feeDistributorAddress = await rocketNodeDistributorFactory.getProxyAddress(node); // console.log(`${node}\t${feeDistributorAddress}`); // continue; const minipoolCount = await rocketMinipoolManager.getNodeMinipoolCount(node); if (minipoolCount > 0) { notInit++; // console.log(`# ${i}: ${averageNodeFee * 100n / '1'.ether}%`); } else { continue; } // console.log(`Minipool count: ${minipoolCount}`); let numerator8 = 0n; let numerator16 = 0n; let active = 0; let finalisedCount = 0; for (let j = 0; j < minipoolCount; j++) { const minipoolAddress = await rocketMinipoolManager.getNodeMinipoolAt(node, j); const minipool = RocketMinipoolDelegate.at(minipoolAddress); const nodeFee = await minipool.getNodeFee(); const finalised = await minipool.getFinalised(); const nodeDepositBalance = await minipool.getNodeDepositBalance(); const status = await minipool.getStatus(); if (finalised) { // console.log(` - ${j}: ${nodeFee} ${nodeDepositBalance} ${status}`); finalisedCount++; } if (status === 2n && !finalised) { if (nodeDepositBalance === '8'.ether) { numerator8 += nodeFee; active++; } else { numerator16 += nodeFee; active++; } } } // console.log(`Active ${active} Finalised ${finalisedCount}`); const numerator16Key = ethers.solidityPackedKeccak256(['string', 'address'], ['node.average.fee.numerator', node]); const numerator8Key = ethers.solidityPackedKeccak256(['string', 'address', 'uint256'], ['node.average.fee.numerator', node, '8'.ether]); const value16 = await rocketStorage.getUint(numerator16Key); const value8 = await rocketStorage.getUint(numerator8Key); // console.log(`numerator8 = ${numerator8} on chain = ${value8}`) // console.log(`numerator16 = ${numerator16} on chain = ${value16}`) // if (numerator8 !== value8 || numerator16 !== value16) { // console.log(`!!! error ${node} ${numerator8} ${value8} ${numerator16} ${value16} ${averageNodeFee} ${active} ${initialised}`); // } else { // console.log(`ok ${node}`); // } if (finalisedCount > 0) { console.log(`!!! error ${node} ${numerator8} ${value8} ${numerator16} ${value16} ${averageNodeFee} ${active} ${initialised}`); } else { console.log(`ok ${node}`); } } // let weightedAverage = 0n; // average = numerator / activeCount; // // if (averageNodeFee !== average) { // console.log('!!!!! Incorrect average') // console.log(`Node: ${node}`); // console.log(`Calculated average: ${average}`) // console.log(`Average node fee: ${averageNodeFee}`); // console.log() // } // console.log(`${i} of ${nodeCount} not init ${notInit}`); // break; } // { // const rocketDAONodeTrusted = RocketDAONodeTrustedUpgrade.at('0x09FB081d4a78cCDf38C6F60a1324fFbC9653f77f'); // const abi = compressABI(loadABI('./contracts/contract/casper/compiled/Deposit.abi')); // // console.log(abi) // await (await rocketDAONodeTrusted.connect(guardian).bootstrapUpgrade("upgradeABI", "casperDeposit", abi, '0x00000000219ab540356cBB839Cbe05303d7705Fa')).wait(); // } // { // const rocketStorage = RocketStorage.at(rocketStorageAddress); // const abi = await rocketStorage.getString(ethers.solidityPackedKeccak256(['string', 'string'], ['contract.abi', 'casperDeposit'])); // const decompressed = decompressABI(abi); // // console.log(decompressed); // } // const blockRoots = artifacts.require('BlockRoots').at('0x581f43e7a51c3E71A0E89Ed7BFf7bB6Fb6B645CE') // // const beaconGenesisTime = 1742213400n; // // const block = await ethers.provider.getBlock(); // const timestamp = BigInt(block.timestamp); // // const currentSlot = (timestamp - beaconGenesisTime) / 12n; // console.log(`Current Slot: `, currentSlot) // console.log(`Current Slot Timestamp: `, timestamp) // console.log(await blockRoots.getBlockRoot(currentSlot-8191n)); // const delegateAbi = artifacts.require('RocketMegapoolDelegate').abi; // const proxyAbi = artifacts.require('RocketMegapoolProxy').abi; // const rocketMegapoolAbi = [...delegateAbi, ...proxyAbi].filter(fragment => fragment.type !== 'constructor'); // console.log(JSON.stringify(rocketMegapoolAbi, null, 2)); // console.log(compressABI(rocketMegapoolAbi)); // const rocketMegapoolProxyAddress = '0xC16eF5B04C1bd583130A1fEc4e8671C895e85074' // const rocketMegapoolFactoryAddress = '0xca3380e1A84AA60A5b6Bfa5F710A33Ad516684Ee'; // await bootstrapUpgrade('upgradeContract', 'rocketMegapoolProxy', compressABI(artifacts.require('RocketMegapoolProxy').abi), rocketMegapoolProxyAddress, { from: guardian }) // await bootstrapUpgrade('upgradeContract', 'rocketMegapoolFactory', compressABI(artifacts.require('RocketMegapoolFactory').abi), rocketMegapoolFactoryAddress, { from: guardian }) // const rocketMegapoolFactory = RocketMegapoolFactory.at('0xca3380e1A84AA60A5b6Bfa5F710A33Ad516684Ee'); // const deployed = await rocketMegapoolFactory.getMegapoolDeployed('0x73abb8ba1DF6F24052eCCe0f58f2208b4Dc43340') // console.log('Is deployed: ' + deployed); // const rocketNetworkRevenues = RocketNetworkRevenues.at('0xf02d2F4bf00972fe990413b712b2394f0B889717') // const blockNumber = await ethers.provider.getBlockNumber() // console.log('Current block: ' + blockNumber); // const split = await rocketNetworkRevenues.calculateSplit(68553n) // console.log(split) // const rocketMegapoolManager = await RocketMegapoolManager.new(rocketStorageAddress) // const address = rocketMegapoolManager.target // const abi = compressABI(RocketMegapoolManager.abi) // const StorageHelper = artifacts.require('StorageHelper') // // const storageHelper = StorageHelper.at('0x53256C6FE23fD9B3D74C7C5994084C47E42631C8').connect(guardian) // bytes32 private constant nodeShareKey = keccak256(abi.encodePacked("network.revenue.node.share")); // bytes32 private constant voterShareKey = keccak256(abi.encodePacked("network.revenue.voter.share")); // bytes32 private constant protocolDAOShareKey = keccak256(abi.encodePacked("network.revenue.pdao.share")); // function decodeSnapshot(encoded) { // const int = BigInt(encoded); // const block = int >> 224n // const value = int & (2n ^ 224n) // return {block, value} // } // // const nodeShareKey = ethers.solidityPackedKeccak256(['string'], ['network.revenue.node.share']) // const pdaoShareKey = ethers.solidityPackedKeccak256(['string'], ['network.revenue.pdao.share']) // const pdaoLengthKey = ethers.solidityPackedKeccak256(['string', 'bytes32'], ['snapshot.length', pdaoShareKey]); // // const nodeSnapshot = await storageHelper.getBytes32(nodeShareKey) // // // // Set snapshot value // // await storageHelper.setBytes32(pdaoShareKey, nodeSnapshot); // // // Set snapshot length // // await storageHelper.setUint(pdaoLengthKey, 1n); // // const pdaoLength = await storageHelper.getUint(pdaoLengthKey); // console.log('New length: ' + pdaoLength) // const pdaoSnapshot = await storageHelper.getBytes32(pdaoShareKey) // console.log('pDAO snapshot: ') // console.log(decodeSnapshot(pdaoSnapshot)); // // const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); // console.log(await rocketDAOProtocolSettingsNetwork.getProtocolDAOShare()); // const rocketNodeTrustedUpgrade = (await RocketDAONodeTrustedUpgrade.deployed()).connect(guardian); // await rocketNodeTrustedUpgrade.bootstrapUpgrade('upgradeContract', 'rocketMegapoolManager', abi, address); // const storageHelper = (await StorageHelper.deployed()).connect(guardian); // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMegapoolManager', compressABI(RocketMegapoolManager.abi), rocketMegapoolManager.target, { from: guardian }); // console.log(JSON.stringify(RocketMegapoolManager.abi, null, 2)) // { // const rocketMegapoolManager = await RocketMegapoolManager.deployed(); // const count = await rocketMegapoolManager.getValidatorCount() // // for (let i = 0n; i < count; i+=1n) { // const validatorInfo = await rocketMegapoolManager.getValidatorInfo(i) // console.log(validatorInfo) // } // } // const storageHelper = (await StorageHelper.at('0x9427b5B5826f4CaDAAe619041920235b552CBed9')).connect(guardian); // // const namespace = ethers.solidityPackedKeccak256(['string'], ['dao.protocol.setting.proposals']) // // async function setUint(namespace, path, value) { // const key = ethers.solidityPackedKeccak256(['bytes32', 'string'], [namespace, path]); // await storageHelper.setUint(key, value) // } // // async function getUint(namespace, path) { // const key = ethers.solidityPackedKeccak256(['bytes32', 'string'], [namespace, path]); // return await storageHelper.getUint(key) // } // // // await setUint(namespace, 'proposal.vote.phase1.time', 86400n) // // await setUint(namespace, 'proposal.vote.phase2.time', 86400n) // // await setUint(namespace, 'proposal.vote.delay.time', 86400n) // // console.log('Vote Phase 1 Time', await getUint(namespace, 'proposal.vote.phase1.time')) // console.log('Vote Phase 2 Time', await getUint(namespace, 'proposal.vote.phase2.time')) // console.log('Vote Delay', await getUint(namespace, 'proposal.vote.delay.time')) // console.log(storageHelper.target); // const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(guardian); // await setDaoNodeTrustedBootstrapMember("rp1", "rocketpool.net", '0xFcB7DC1BAEE1651137F9C0ea6F2E650359aDe290', { from: guardian }) // try { // await setDaoNodeTrustedBootstrapMember("rp2", "rocketpool.net", '0x9D5e78c8c8C30793F142dCc68e759B69196f6886', { from: guardian }) // } catch(e) {} // try { // await setDaoNodeTrustedBootstrapMember("rp3", "rocketpool.net", '0xA3819Ae703Fe88A5E6a917d0aF2c88a453bDfa44', { from: guardian }) // } catch(e) {} // await rocketDAOProtocol.bootstrapEnableGovernance(); // await rocketDAOProtocol.connect(guardian).bootstrapSettingUint('rocketDAOProtocolSettingsNode', 'node.unstaking.period', (60 * 15)); // const rocketDepositPool = RocketDepositPool.at('0xB207A827E6a37259A9331238e02f57652e584509'); // // const megapool = RocketMegapoolDelegate.at("0x3EB98dd3d1303E5e18819761661ef964e2a56B9E"); // // console.log(await megapool.getActiveValidatorCount()); // // console.log(await megapool.getValidatorCount()); // // console.log('Standard queue length: ' + await rocketDepositPool.getStandardQueueLength()); // console.log('Express queue length: ' + await rocketDepositPool.getExpressQueueLength()); // for (let i = 0n; i < 3n; i += 1n) { // console.log(`Validator in position ${i+1n} is ` + await calculatePositionInQueue(megapool, i+1n)) // } // const megapoolAddress = '0x99a96EafFAcafA02ad45eF0Ea241D570A35A4519'; // console.log('Position in standard queue for validator 1 is ' + await findInQueue(linkedListStorage, megapool.target, 1n, 'deposit.queue.standard')) // console.log('Position in express queue for for validator 2 is ' + await findInQueue(linkedListStorage, megapool.target, 2n, 'deposit.queue.express')) // console.log('Position in standard queue for validator 3 is ' + await findInQueue(linkedListStorage, megapool.target, 3n, 'deposit.queue.standard')) // const expressQueueKey = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.express']) // const standardQueueKey = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.standard']) // // const results = await linkedListStorage.scan(expressQueueKey, 0, 20); // // // // // console.log(results[0].length); // console.log(results[0]); //0x8FCC27e7497A0968e105b83a7dAB3BfE1171C97d // uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); // const data = await rocketStorage.getUint(ethers.solidityPackedKeccak256(['bytes32', 'string'], [standardQueueKey, '.data'])); // const queueLen = (data >> lengthOffset) & uint64Mask; // const queueStart = (data >> startOffset) & uint64Mask; // const queueEnd = (data >> endOffset) & uint64Mask; // // console.log(queueStart) // console.log(queueEnd) // console.log(queueLen) // // for (let i = 0n; i < queueEnd + 1n; i += 1n) { // const dataKey = ethers.solidityPackedKeccak256(['bytes32', 'string', 'uint256'], [standardQueueKey, '.item', i]); // const nextKey = ethers.solidityPackedKeccak256(['bytes32', 'string', 'uint256'], [standardQueueKey, '.next', i]); // const packedData = await rocketStorage.getUint(dataKey); // const next = await rocketStorage.getUint(nextKey); // console.log(`${i}: ${next} = ${packedData}`); // } // // const megapool = RocketMegapoolDelegate.at('0x99a96eaffacafa02ad45ef0ea241d570a35a4519'); // const rewards = await megapool.calculateRewards('1'.ether); // console.log(rewards); // console.log(await megapool.getPendingRewards()); // const key = ethers.solidityPackedKeccak256(['string', 'uint256'], ['megapool.validator.set', 1]) // const rocketStorage = await RocketStorage.at(rocketStorageAddress); // console.log(await rocketStorage.getUint(key)); // process.exit(0); // Deploy upgrade helper // { // // const rocketDepositPool = await RocketDepositPool.new(rocketStorageAddress); // console.log(`Deployed deposit pool to: ${rocketDepositPool.target}`) // // const rocketNodeDeposit = await RocketNodeDeposit.new(rocketStorageAddress); // console.log(`Deployed node deposit to: ${rocketNodeDeposit.target}`) // // const upgradeHelper = await MegapoolUpgradeHelper.new(rocketStorageAddress); // console.log(`Deployed upgrade helper to: ${upgradeHelper.target}`) // // await setDaoNodeTrustedBootstrapUpgrade('addContract', 'upgradeHelper', compressABI(MegapoolUpgradeHelper.abi), upgradeHelper.target, { from: guardian }); // const rocketMegapoolDelegate = await RocketMegapoolDelegate.new(rocketStorageAddress); // console.log(`Deployed megapool delegate to: ${rocketMegapoolDelegate.target}`) // const upgradeHelperAddress = '0xf2E953D6973B5d657f758E4FC3c27A7CBc1879E4'; // const upgrader = MegapoolUpgradeHelper.at(upgradeHelperAddress).connect(guardian); // await upgrader.upgradeDelegate('0x0097dA0269584dA4188b59440AC266E0AF93A9E7') // const linkedListStorage = await LinkedListStorage.new(rocketStorageAddress); // console.log(`Deployed linked list storage: ${linkedListStorage.target}`) // // const rocketNodeManager = await RocketNodeManager.new(rocketStorageAddress); // console.log(`Deployed rocket node manager: ${rocketNodeManager.target}`) // const blockRoots = BlockRoots.at('0x358E0964A806Bb9F10421D5d34d8174A85FA66E3') // const timestamp = await blockRoots.getTimestampFromSlot(572341n) // const root = await blockRoots.getBlockRoot(572340n); // console.log('Timestamp: ' + timestamp) // console.log('Root: ' + root) // constructor(uint256 _genesisBlockTimestamp, uint256 _secondsPerSlot, uint256 _beaconRootsHistoryBufferLength, address _beaconRoots) { // genesisBlockTimestamp: 1742213400n, // secondsPerSlot: 12n, // beaconRootsHistoryBufferLength: 8192n, // historicalRootOffset: 0n, // beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', // const rocketDAOProtocolSettingsMegapool = await RocketDAOProtocolSettingsMegapool.new(rocketStorageAddress); // console.log(`Deployed rocket dao protocol settings megapool: ${rocketDAOProtocolSettingsMegapool.target}`) // // await artifacts.loadFromDeployment(rocketStorageAddress); // const rocketDepositPool = RocketDepositPool.at('0x04b72d20067d5bebefd0ed2a83ade58468e4cfcd'); // console.log(await rocketDepositPool.getStandardQueueLength()); // const rocketDepositPool = RocketDepositPool.at("0xB20d5dcd8c227c3Ca17aAef34dB8C027a49a5f70"); // console.log(await rocketDepositPool.getQueueTop()); // const BlockRoots = await artifacts.require('BlockRoots') // const blockRoots = '0x358E0964A806Bb9F10421D5d34d8174A85FA66E3' // const BlockRoots = await artifacts.require('BlockRoots') // const blockRoots = await BlockRoots.new( // 1742213400n, // 12n, // 8191n, // '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02' // ) // // const BeaconStateVerifier = await artifacts.require('BeaconStateVerifier') // const beaconStateVerifier = await BeaconStateVerifier.new(rocketStorageAddress, 8192n, 0n); // try { // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'blockRoots', compressABI(BlockRoots.abi), blockRoots.target, { from: guardian }); // } catch (e) {} // try { // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'beaconStateVerifier', compressABI(BeaconStateVerifier.abi), beaconStateVerifier.target, { from: guardian }); // } catch (e) {} // try { // } catch (e) {} // const upgradeHelper = MegapoolUpgradeHelper.at('0xcf71f8e767B6520936bDF54c2F68B7A28e8366ef'); // await upgradeHelper.connect(guardian).upgradeDelegate('0x63782525426EAc34B76a0C4843B4aC73aD593a7E'); // try { // } catch(e){ // console.error(e); // } // await rocketDepositPool.connect(guardian).fixNodeCredit('0x462eb18d5c8AEb77FB3cd82E621bAE81E13F4Ce9') // await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketDAOProtocolSettingsMegapool', compressABI(RocketDAOProtocolSettingsMegapool.abi), rocketDAOProtocolSettingsMegapool.target, { from: guardian }); // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'linkedListStorage', compressABI(LinkedListStorage.abi), '0x090d5B19933C64721eCEE4b5E14F602e98032c19', { from: guardian }); // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', compressABI(RocketNodeManager.abi), '0xdD53A327b8C615EBE2b1f2F3aB19fCb7b98C838A', { from: guardian }); // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMegapoolManager', compressABI(RocketMegapoolManager.abi), '0xc949B6f3d46d7366462D875B7e81642C1c90f4e8', { from: guardian }); // *** Deploy megapool // { // const rocketMegapoolDelegate = await artifacts.require('RocketMegapoolDelegate'); // const instance = await rocketMegapoolDelegate.new(rocketStorageAddress); // console.log(`Deployed megapool delegate to: ${instance.target}`) // // const megapoolUpgradeHelper = await artifacts.require('MegapoolUpgradeHelper'); // const upgradeHelperAddress = '0x94b9C82d6342c47D31186d3F40fA8b6035045CFC'; // const upgrader = megapoolUpgradeHelper.at(upgradeHelperAddress).connect(guardian); // await upgrader.upgradeDelegate(instance.target) // } // // *** Upgrade Delegate // const megapoolUpgradeHelper = await artifacts.require('MegapoolUpgradeHelper'); // const upgradeHelperAddress = '0x94b9C82d6342c47D31186d3F40fA8b6035045CFC'; // const instance = megapoolUpgradeHelper.at(upgradeHelperAddress).connect(guardian); // await instance.upgradeDelegate('0xA4aCdcC348c7974592305fbfBD87AC0D8B480753') // // const delegateAbi = artifacts.require('RocketMegapoolDelegate').abi; // const proxyAbi = artifacts.require('RocketMegapoolProxy').abi; // const rocketMegapoolAbi = [...delegateAbi, ...proxyAbi].filter(fragment => fragment.type !== 'constructor'); // console.log(JSON.stringify(rocketMegapoolAbi, null, 2)); // console.log(compressABI(rocketMegapoolAbi)); // // await artifacts.loadFromDeployment(rocketStorageAddress); // Deploy upgrade helper // { // const instance = await RocketMegapoolManager.new(rocketStorageAddress); // console.log(`Deployed manager to: ${instance.target}`) // } // const rocketDepositPool = RocketDepositPool.at('0x4Cd86a9583Bf779F695c24E15D1bdFf660A69295'); // console.log(await rocketDepositPool.getNodeCreditBalance("0x462eb18d5c8AEb77FB3cd82E621bAE81E13F4Ce9")); // const linkedListABI = LinkedListStorage.abi; // const rocketMegapoolManagerABI = RocketMegapoolManager.abi; // const rocketNetworkRevenuesABI = RocketNetworkRevenues.abi; // // await artifacts.loadFromDeployment(rocketStorageAddress); // // await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'linkedListStorage', linkedListABI, '0x0000000000000000000000000000000000000000', { from: guardian }); // try { // await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketMegapoolManager', rocketMegapoolManagerABI, '0x0000000000000000000000000000000000000000', { from: guardian }); // } catch(e){} // try { // await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketNetworkRevenues', rocketNetworkRevenuesABI, '0x0000000000000000000000000000000000000000', { from: guardian }); // } catch(e){} // await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMegapoolManager', compressABI(RocketMegapoolManager.abi), '0xEc9e119D0d1cE5ce00e86a955325B3DeB2230cD8', { from: guardian }); // // // console.log(JSON.stringify(rocketMegapoolAbi)); // const compressed = compressABI(rocketMegapoolAbi); // console.log('Compressed string is:') // console.log(compressed) // // console.log(compressABI(rocketMegapoolAbi)); // // await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketMegapool', compressed, '0xd4F8a817821393b2020eC32a404D2931dfb09700', { from: guardian }); // await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.node.commission.share", '0.10'.ether, { from: guardian }); // const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(guardian); // const newValue = ethers.parseUnits('0.10', 'ether'); // console.log(newValue); // const rocketDAOProtocol = await RocketDAOProtocol.deployed(); // await rocketDAOProtocol.connect(guardian).bootstrapSettingUint('rocketDAOProtocolSettingsNode', 'node.unstaking.period', (60 * 15)); // console.log(tx); } go().then(() => process.exit(0)); ================================================ FILE: scripts/deploy-upgrade.v1.4.js ================================================ const pako = require('pako'); const { RocketDAONodeTrusted, RocketUpgradeOneDotFour, artifacts } = require('../test/_utils/artifacts.js'); const hre = require('hardhat'); const { EtherscanVerifier } = require('../test/_helpers/verify'); const fs = require('fs'); const path = require('path'); const ethers = hre.ethers; function compressABI(abi) { return Buffer.from(pako.deflate(JSON.stringify(abi))).toString('base64'); } function formatConstructorArgs(args) { return JSON.stringify(args, (key, value) => typeof value === 'bigint' ? value.toString() : value, ); } const rocketStorageAddress = process.env.ROCKET_STORAGE; let rocketUpgradeAddress; const CHAINS = { 'hoodi': { genesisBlockTimestamp: 1742213400n, slotsPerHistoricalRoot: 8192n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', genesisValidatorRoot: '0x212f13fc4df078b6cb7db228f1c8307566dcecf900867401a92023d7ba99cb5f', forkSlots: [ 0n, // Altair 0n, // Bellatrix 0n, // Capella 0n, // Deneb 2048n * 32n, // Electra ], }, 'mainnet': { genesisBlockTimestamp: 1606824023n, slotsPerHistoricalRoot: 8192n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', genesisValidatorRoot: '0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95', forkSlots: [ 74240n * 32n, // Altair 144896n * 32n, // Bellatrix 194048n * 32n, // Capella 269568n * 32n, // Deneb 364032n * 32n, // Electra ], }, 'private': { genesisBlockTimestamp: 1762861080n, slotsPerHistoricalRoot: 8192n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', genesisValidatorRoot: '0xec9caf9aad26d20776fbd9e03b61dee7e7bd155a32d1593d43c47df730c40f14', forkSlots: [ 0n, // Altair 0n, // Bellatrix 0n, // Capella 0n, // Deneb 0n, // Electra ], }, }; const networkContracts = { rocketMegapoolDelegate: artifacts.require('RocketMegapoolDelegate'), rocketMegapoolFactory: artifacts.require('RocketMegapoolFactory'), rocketMegapoolProxy: artifacts.require('RocketMegapoolProxy'), rocketMegapoolManager: artifacts.require('RocketMegapoolManager'), rocketNodeManager: artifacts.require('RocketNodeManager'), rocketNodeDeposit: artifacts.require('RocketNodeDeposit'), rocketNodeStaking: artifacts.require('RocketNodeStaking'), rocketDepositPool: artifacts.require('RocketDepositPool'), linkedListStorage: artifacts.require('LinkedListStorage'), rocketDAOProtocol: artifacts.require('RocketDAOProtocol'), rocketDAOProtocolProposals: artifacts.require('RocketDAOProtocolProposals'), rocketDAOProtocolSettingsNode: artifacts.require('RocketDAOProtocolSettingsNode'), rocketDAOProtocolSettingsDeposit: artifacts.require('RocketDAOProtocolSettingsDeposit'), rocketDAOProtocolSettingsNetwork: artifacts.require('RocketDAOProtocolSettingsNetwork'), rocketDAOProtocolSettingsSecurity: artifacts.require('RocketDAOProtocolSettingsSecurity'), rocketDAOProtocolSettingsMegapool: artifacts.require('RocketDAOProtocolSettingsMegapool'), rocketDAOProtocolSettingsMinipool: artifacts.require('RocketDAOProtocolSettingsMinipool'), rocketDAOSecurityUpgrade: artifacts.require('RocketDAOSecurityUpgrade'), rocketDAOSecurityProposals: artifacts.require('RocketDAOSecurityProposals'), rocketDAONodeTrustedUpgrade: artifacts.require('RocketDAONodeTrustedUpgrade'), rocketNetworkRevenues: artifacts.require('RocketNetworkRevenues'), rocketNetworkBalances: artifacts.require('RocketNetworkBalances'), rocketNetworkSnapshots: artifacts.require('RocketNetworkSnapshots'), rocketNetworkPenalties: artifacts.require('RocketNetworkPenalties'), rocketRewardsPool: artifacts.require('RocketRewardsPool'), beaconStateVerifier: artifacts.require('BeaconStateVerifier'), rocketNodeDistributorDelegate: artifacts.require('RocketNodeDistributorDelegate'), rocketClaimDAO: artifacts.require('RocketClaimDAO'), rocketMinipoolBondReducer: artifacts.require('RocketMinipoolBondReducer'), rocketMinipoolManager: artifacts.require('RocketMinipoolManager'), rocketNetworkVoting: artifacts.require('RocketNetworkVoting'), rocketMerkleDistributorMainnet: artifacts.require('RocketMerkleDistributorMainnet'), rocketMegapoolPenalties: artifacts.require('RocketMegapoolPenalties'), rocketNetworkSnapshotsTime: artifacts.require('RocketNetworkSnapshotsTime'), rocketDAOProtocolSettingsProposals: artifacts.require('RocketDAOProtocolSettingsProposals'), }; async function deployUpgrade(rocketStorageAddress) { const [signer] = await ethers.getSigners(); const genesisBlockTimestamp = CHAINS[process.env.CHAIN].genesisBlockTimestamp; const slotsPerHistoricalRoot = CHAINS[process.env.CHAIN].slotsPerHistoricalRoot; const beaconRoots = CHAINS[process.env.CHAIN].beaconRoots; const forkSlots = CHAINS[process.env.CHAIN].forkSlots; const genesisValidatorRoot = CHAINS[process.env.CHAIN].genesisValidatorRoot; const deployedContracts = {}; const contractPlan = {}; const deployTxs = [] async function deployNetworkContract(name) { const plan = contractPlan[name]; if (!plan) { throw Error(`No contract deployment plan for ${name}`); } let artifact = plan.artifact; let abi = artifact.abi; console.log(`- Deploying "${name}"`); let constructorArgs = typeof plan.constructorArgs === 'function' ? plan.constructorArgs() : plan.constructorArgs; console.log(`- Constructor args = ${formatConstructorArgs(constructorArgs)}`); // Deploy and log result const instance = await artifact.newImmediate(...constructorArgs); const rsTx = await instance.deploymentTransaction(); const address = instance.target; console.log(`Deployed to ${address} @ ${rsTx.hash}`); console.log(); // Encode the constructor args const iface = new ethers.Interface(abi); const encodedConstructorArgs = iface.encodeDeploy(constructorArgs); // Add to deployed contracts deployedContracts[name] = { artifact: artifact, constructorArgs: encodedConstructorArgs, abi: abi, address: address, instance: instance, }; deployTxs.push(rsTx); } // Setup contract plan for (let contract in networkContracts) { switch (contract) { case 'rocketNodeDistributorDelegate': contractPlan[contract] = { artifact: networkContracts[contract], constructorArgs: [], }; break; case 'beaconStateVerifier': contractPlan[contract] = { artifact: networkContracts[contract], constructorArgs: [rocketStorageAddress, slotsPerHistoricalRoot, forkSlots, beaconRoots, genesisBlockTimestamp, genesisValidatorRoot], }; break; // All other contracts - pass storage address default: contractPlan[contract] = { artifact: networkContracts[contract], constructorArgs: [rocketStorageAddress], }; break; } } contractPlan['RocketUpgradeOneDotFour'] = { artifact: artifacts.require('RocketUpgradeOneDotFour'), constructorArgs: () => { return [ rocketStorageAddress, ]; }, }; // Deploy contracts for (let contract in networkContracts) { await deployNetworkContract(contract); } // Deploy upgrade await deployNetworkContract('RocketUpgradeOneDotFour'); // Wait for all contract to be deployed await Promise.all(deployTxs.map(deployTx => deployTx.wait())) // Set const upgradeContract = deployedContracts['RocketUpgradeOneDotFour'].instance; const setAddressesA = [ deployedContracts.rocketMegapoolDelegate.address, deployedContracts.rocketMegapoolFactory.address, deployedContracts.rocketMegapoolProxy.address, deployedContracts.rocketMegapoolManager.address, deployedContracts.rocketNodeManager.address, deployedContracts.rocketNodeDeposit.address, deployedContracts.rocketNodeStaking.address, deployedContracts.rocketDepositPool.address, deployedContracts.linkedListStorage.address, deployedContracts.rocketDAOProtocol.address, deployedContracts.rocketDAOProtocolProposals.address, deployedContracts.rocketDAOProtocolSettingsNode.address, deployedContracts.rocketDAOProtocolSettingsDeposit.address, deployedContracts.rocketDAOProtocolSettingsNetwork.address, deployedContracts.rocketDAOProtocolSettingsSecurity.address, deployedContracts.rocketDAOProtocolSettingsMegapool.address, deployedContracts.rocketDAOProtocolSettingsMinipool.address, ]; const setAddressesB = [ deployedContracts.rocketDAOSecurityUpgrade.address, deployedContracts.rocketDAOSecurityProposals.address, deployedContracts.rocketDAONodeTrustedUpgrade.address, deployedContracts.rocketNetworkRevenues.address, deployedContracts.rocketNetworkBalances.address, deployedContracts.rocketNetworkSnapshots.address, deployedContracts.rocketNetworkPenalties.address, deployedContracts.rocketRewardsPool.address, deployedContracts.beaconStateVerifier.address, deployedContracts.rocketNodeDistributorDelegate.address, deployedContracts.rocketClaimDAO.address, deployedContracts.rocketMinipoolBondReducer.address, deployedContracts.rocketMinipoolManager.address, deployedContracts.rocketNetworkVoting.address, deployedContracts.rocketMerkleDistributorMainnet.address, deployedContracts.rocketMegapoolPenalties.address, deployedContracts.rocketNetworkSnapshotsTime.address, deployedContracts.rocketDAOProtocolSettingsProposals.address, ]; const setAbisA = [ compressABI(networkContracts.rocketMegapoolDelegate.abi), compressABI(networkContracts.rocketMegapoolFactory.abi), compressABI(networkContracts.rocketMegapoolProxy.abi), compressABI(networkContracts.rocketMegapoolManager.abi), compressABI(networkContracts.rocketNodeManager.abi), compressABI(networkContracts.rocketNodeDeposit.abi), compressABI(networkContracts.rocketNodeStaking.abi), compressABI(networkContracts.rocketDepositPool.abi), compressABI(networkContracts.linkedListStorage.abi), compressABI(networkContracts.rocketDAOProtocol.abi), compressABI(networkContracts.rocketDAOProtocolProposals.abi), compressABI(networkContracts.rocketDAOProtocolSettingsNode.abi), compressABI(networkContracts.rocketDAOProtocolSettingsDeposit.abi), compressABI(networkContracts.rocketDAOProtocolSettingsNetwork.abi), compressABI(networkContracts.rocketDAOProtocolSettingsSecurity.abi), compressABI(networkContracts.rocketDAOProtocolSettingsMegapool.abi), compressABI(networkContracts.rocketDAOProtocolSettingsMinipool.abi), ]; const setAbisB = [ compressABI(networkContracts.rocketDAOSecurityUpgrade.abi), compressABI(networkContracts.rocketDAOSecurityProposals.abi), compressABI(networkContracts.rocketDAONodeTrustedUpgrade.abi), compressABI(networkContracts.rocketNetworkRevenues.abi), compressABI(networkContracts.rocketNetworkBalances.abi), compressABI(networkContracts.rocketNetworkSnapshots.abi), compressABI(networkContracts.rocketNetworkPenalties.abi), compressABI(networkContracts.rocketRewardsPool.abi), compressABI(networkContracts.beaconStateVerifier.abi), compressABI(networkContracts.rocketNodeDistributorDelegate.abi), compressABI(networkContracts.rocketClaimDAO.abi), compressABI(networkContracts.rocketMinipoolBondReducer.abi), compressABI(networkContracts.rocketMinipoolManager.abi), compressABI(networkContracts.rocketNetworkVoting.abi), compressABI(networkContracts.rocketMerkleDistributorMainnet.abi), compressABI(networkContracts.rocketMegapoolPenalties.abi), compressABI(networkContracts.rocketNetworkSnapshotsTime.abi), compressABI(networkContracts.rocketDAOProtocolSettingsProposals.abi), ]; await upgradeContract.connect(signer).setA(setAddressesA, setAbisA); await upgradeContract.connect(signer).setB(setAddressesB, setAbisB); return deployedContracts; } async function deploy() { const [signer] = await ethers.getSigners(); console.log(); console.log('# Deploying'); console.log(); console.log(` - Deploying from ${signer.address}`); // Deploy upgrade { const contracts = await deployUpgrade(rocketStorageAddress); // Compile deployment information for saving const deploymentData = { deployer: signer.address, chain: process.env.CHAIN, verification: [], addresses: {}, buildInfos: {}, }; // Compile set of build infos const buildInfoMap = {}; for (const contract in contracts) { const artifact = contracts[contract].artifact; const buildInfo = hre.artifacts.getBuildInfoSync(`${artifact.sourceName}:${artifact.contractName}`); deploymentData.buildInfos[buildInfo.id] = buildInfo; buildInfoMap[contract] = buildInfo.id; } // Compile list of information needed for verification for (const contract in contracts) { const artifact = contracts[contract].artifact; deploymentData.verification.push({ sourceName: artifact.sourceName, contractName: artifact.contractName, address: contracts[contract].address, constructorArgs: contracts[contract].constructorArgs, buildInfoId: buildInfoMap[contract], }); deploymentData.addresses[artifact.contractName] = contracts[contract].address; } // Save deployment data const deployFile = 'deployments' + path.sep + process.env.CHAIN + '_' + (new Date().toISOString()) + '.json'; if (!fs.existsSync('deployments')) { fs.mkdirSync('deployments'); } const jsonDeploymentData = JSON.stringify(deploymentData, null, 2); fs.writeFileSync(deployFile, jsonDeploymentData, 'utf8'); fs.writeFileSync('deployments' + path.sep + 'latest.json', jsonDeploymentData, 'utf8'); console.log(' - Deployment data saved to `' + deployFile + '`'); rocketUpgradeAddress = contracts['RocketUpgradeOneDotFour'].address; console.log(` - Upgrade contract deployed to: ${rocketUpgradeAddress}`); } } async function bootstrap() { console.log(); console.log('# Bootstrapping upgrade'); console.log(); const [signer] = await ethers.getSigners(); await artifacts.loadFromDeployment(rocketStorageAddress); const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); await rocketDAONodeTrusted.connect(signer).bootstrapUpgrade('addContract', 'rocketUpgradeOneDotFour', compressABI(RocketUpgradeOneDotFour.abi), rocketUpgradeAddress); } async function execute() { console.log(); console.log('# Executing upgrade'); console.log(); const [signer] = await ethers.getSigners(); const upgradeContract = await RocketUpgradeOneDotFour.at(rocketUpgradeAddress); await upgradeContract.connect(signer).execute(); } async function verify() { const deploymentData = JSON.parse(fs.readFileSync('deployments/latest.json').toString('utf-8')); // Verify all deployed contracts const verifierOpts = { chain: process.env.CHAIN, preamble: process.env.PREAMBLE !== null ? fs.readFileSync(process.cwd() + path.sep + process.env.PREAMBLE, 'utf8') : '', apiKey: process.env.ETHERSCAN_API_KEY, }; const verifier = new EtherscanVerifier(deploymentData.buildInfos, verifierOpts); const verificationResults = await verifier.verifyAll(deploymentData.verification); console.log(); console.log('# Verification results'); console.log(); for (const contract in verificationResults) { const guid = verificationResults[contract]; if (guid === null) { console.log(` - ${contract}: Failed to submit`); } else { const status = await verifier.getVerificationStatus(verificationResults[contract]); console.log(` - ${contract}: ${status.result}`); } await new Promise(resolve => setTimeout(resolve, 500)); } console.log(); } async function go() { // Deploy contracts await deploy(); // Optionally verify on Etherscan if (process.env.VERIFY === 'true') { await verify(); } // Bootstrap upgrade contract if (process.env.BOOTSTRAP === 'true') { await bootstrap(); // Execute upgrade if (process.env.EXECUTE === 'true') { await execute(); } } } go().then(() => process.exit(0)); ================================================ FILE: scripts/deploy.js ================================================ import { RocketPoolDeployer } from '../test/_helpers/deployer'; import { artifacts } from '../test/_utils/artifacts'; import { injectBNHelpers } from '../test/_helpers/bn'; import { EtherscanVerifier } from '../test/_helpers/verify'; import fs from 'fs'; import path from 'path'; const hre = require('hardhat'); const ethers = hre.ethers; const chain = process.env.CHAIN || 'mainnet'; const verify = process.env.VERIFY === 'true' || false; const preamble = process.env.PREAMBLE || null; const etherscanApiKey = process.env.ETHERSCAN_API_KEY || null; const chainOpts = { 'mainnet': { deployer: { depositAddress: '0x00000000219ab540356cBB839Cbe05303d7705Fa', rocketTokenRPLFixedSupply: '0xb4efd85c19999d84251304bda99e90b92300bd93', genesisBlockTimestamp: 1695902400n, secondsPerSlot: 12n, slotsPerHistoricalRoot: 8192n, beaconRootsHistoryBufferLength: 8191n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', forkSlots: [ 74240n * 32n, // Altair 144896n * 32n, // Bellatrix 194048n * 32n, // Capella 269568n * 32n, // Deneb 364032n * 32n, // Electra ] }, deployStorageHelper: false, mintDRPL: false, setDefaults: false, }, 'hoodi': { deployer: { depositAddress: '0x00000000219ab540356cBB839Cbe05303d7705Fa', rocketTokenRPLFixedSupply: null, genesisBlockTimestamp: 1742213400n, secondsPerSlot: 12n, slotsPerHistoricalRoot: 8192n, beaconRootsHistoryBufferLength: 8191n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', forkSlots: [ 0n, // Altair 0n, // Bellatrix 0n, // Capella 0n, // Deneb 2048n * 32n, // Electra ] }, deployStorageHelper: true, mintDRPL: true, setDefaults: true, }, 'private': { deployer: { depositAddress: '0x00000000219ab540356cBB839Cbe05303d7705Fa', rocketTokenRPLFixedSupply: null, genesisBlockTimestamp: 1761825733n, secondsPerSlot: 12n, slotsPerHistoricalRoot: 8192n, beaconRootsHistoryBufferLength: 8191n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', genesisValidatorRoot: '0xec9caf9aad26d20776fbd9e03b61dee7e7bd155a32d1593d43c47df730c40f14', forkSlots: [ 0n, // Altair 0n, // Bellatrix 0n, // Capella 0n, // Deneb 0n, // Electra ], }, deployStorageHelper: true, mintDRPL: true, setDefaults: true, }, 'hardhat': { deployer: { logging: false }, deployStorageHelper: false, mintDRPL: false, setDefaults: false, }, }; injectBNHelpers(); async function deploy() { const opts = chainOpts[chain]; const [signer] = await ethers.getSigners(); const deployer = new RocketPoolDeployer(signer, opts.deployer); // Add storageHelper to deployment plan if (opts.deployStorageHelper) { deployer.contractPlan['storageHelper'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('StorageHelper'), }; } // Bootstrap default parameters if (opts.setDefaults) { deployer.addStage('Set default parameters', 110, [ async () => { await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsDeposit', 'deposit.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsDeposit', 'deposit.assign.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsDeposit', 'deposit.pool.maximum', '1000'.ether); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNode', 'node.registration.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNode', 'node.deposit.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNode', 'node.vacant.minipools.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsMinipool', 'minipool.submit.withdrawable.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsMinipool', 'minipool.bond.reduction.enabled', true); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNetwork', 'network.node.fee.minimum', '0.05'.ether); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNetwork', 'network.node.fee.target', '0.1'.ether); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNetwork', 'network.node.fee.maximum', '0.2'.ether); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsNetwork', 'network.node.demand.range', '1000'.ether); await deployer.bootstrapProtocolDAOSetting('rocketDAOProtocolSettingsInflation', 'rpl.inflation.interval.start', Math.floor(new Date().getTime() / 1000) + (60 * 60 * 24 * 14)); await deployer.bootstrapProtocolDAOClaimers('0.275'.ether, '0.025'.ether, '0.7'.ether); }, ]); } // Mint the total DRPL supply to the deployer if (opts.mintDRPL) { deployer.addStage('Mint DRPL supply', 120, [ async () => { const rocketTokenRPLFixedSupply = deployer.deployedContracts['rocketTokenRPLFixedSupply'].instance; const totalSupplyCap = await rocketTokenRPLFixedSupply.totalSupplyCap(); deployer.log(`- Minting ${ethers.formatEther(totalSupplyCap)} DRPL to ${signer.address}`, 'white'); await rocketTokenRPLFixedSupply.mint(signer.address, totalSupplyCap); }, ]); } const balance = await ethers.provider.getBalance(signer.address); console.log(`Chain: ${chain}`); console.log(`Deployer: ${signer.address}`); console.log(`Deployer Balance: ${ethers.formatEther(balance)} ETH`); console.log('\n'); // Perform deployment const contracts = await deployer.deploy(); // Skip save and verify when deploying to hardhat if (chain === 'hardhat') { return; } // Compile deployment information for saving const deploymentData = { deployer: signer.address, chain: chain, verification: [], addresses: {}, buildInfos: {}, }; // Compile set of build infos const buildInfoMap = {}; for (const contract in contracts) { const artifact = contracts[contract].artifact; const buildInfo = hre.artifacts.getBuildInfoSync(`${artifact.sourceName}:${artifact.contractName}`); deploymentData.buildInfos[buildInfo.id] = buildInfo; buildInfoMap[contract] = buildInfo.id; } // Compile list of information needed for verification for (const contract in contracts) { const artifact = contracts[contract].artifact; deploymentData.verification.push({ sourceName: artifact.sourceName, contractName: artifact.contractName, address: contracts[contract].address, constructorArgs: contracts[contract].constructorArgs, buildInfoId: buildInfoMap[contract], }); deploymentData.addresses[artifact.contractName] = contracts[contract].address; } // Save deployment data const deployFile = 'deployments' + path.sep + chain + '_' + (new Date().toISOString()) + '.json'; if (!fs.existsSync('deployments')) { fs.mkdirSync('deployments'); } const jsonDeploymentData = JSON.stringify(deploymentData, null, 2); fs.writeFileSync(deployFile, jsonDeploymentData, 'utf8'); fs.writeFileSync('deployments' + path.sep + 'latest.json', jsonDeploymentData, 'utf8'); console.log('Deployment data saved to `' + deployFile + '`'); console.log(); // Optionally start verification process if (verify) { // Verify all deployed contracts const verifierOpts = { chain: chain, preamble: preamble !== null ? fs.readFileSync(process.cwd() + path.sep + preamble, 'utf8') : '', apiKey: etherscanApiKey, }; const verifier = new EtherscanVerifier(deploymentData.buildInfos, verifierOpts); const verificationResults = await verifier.verifyAll(deploymentData.verification); console.log(); console.log('# Verification results') console.log(); for (const contract in verificationResults) { const guid = verificationResults[contract]; if (guid === null) { console.log(` - ${contract}: Failed to submit`); } else { const status = await verifier.getVerificationStatus(verificationResults[contract]); console.log(` - ${contract}: ${status.result}`); } } console.log(); } console.log('# Deployment complete'); } deploy().then(() => process.exit()); ================================================ FILE: scripts/etherscan-verify.js ================================================ import { EtherscanVerifier } from '../test/_helpers/verify'; import fs from 'fs'; import path from 'path'; const preamble = process.env.PREAMBLE || null; const etherscanApiKey = process.env.ETHERSCAN_API_KEY || null; const deployment = process.env.DEPLOYMENT || 'latest'; async function verify() { const deploymentData = JSON.parse(fs.readFileSync('deployments' + path.sep + deployment + '.json').toString('utf-8')); const chain = deploymentData.chain; console.log(`Chain: ${chain}`); console.log('\n'); // Verify all deployed contracts const verifierOpts = { chain: chain, preamble: preamble !== null ? fs.readFileSync(process.cwd() + path.sep + preamble, 'utf8') : '', apiKey: etherscanApiKey, }; const verifier = new EtherscanVerifier(deploymentData.buildInfos, verifierOpts); const verificationResults = await verifier.verifyAll(deploymentData.verification); console.log(); console.log('# Verification results'); console.log(); for (const contract in verificationResults) { const guid = verificationResults[contract]; if (guid === null) { console.log(` - ${contract}: Failed to submit`); } else if (guid === undefined) { console.log(` - ${contract}: Already verified`); } else { const status = await verifier.getVerificationStatus(verificationResults[contract]); console.log(` - ${contract}: ${status.result}`); } } } verify().then(() => process.exit()); ================================================ FILE: scripts/preamble.sol ================================================ /** * . * / \ * |.'.| * |'.'| * ,'| |'. * |,-'-|-'-.| * __|_| | _ _ _____ _ * | ___ \| | | | | | ___ \ | | * | |_/ /|__ ___| | _____| |_ | |_/ /__ ___ | | * | // _ \ / __| |/ / _ \ __| | __/ _ \ / _ \| | * | |\ \ (_) | (__| < __/ |_ | | | (_) | (_) | | * \_| \_\___/ \___|_|\_\___|\__| \_| \___/ \___/|_| * +---------------------------------------------------+ * | DECENTRALISED STAKING PROTOCOL FOR ETHEREUM | * +---------------------------------------------------+ * * Rocket Pool is a first-of-its-kind Ethereum staking pool protocol, designed to * be community-owned, decentralised, permissionless, & trustless. * * For more information about Rocket Pool, visit https://rocketpool.net * * Authored by the Rocket Pool Core Team * Contributors: https://github.com/rocket-pool/rocketpool/graphs/contributors * A special thanks to the Rocket Pool community for all their contributions. * */ ================================================ FILE: scripts/upgrade-test.sh ================================================ #!/bin/bash # Point hardhat at local node export PROVIDER_URL=http://localhost:8545 export MNEMONIC="test test test test test test test test test test test junk" export MNEMONIC_PASSWORD= export ROCKET_STORAGE=0x5FbDB2315678afecb367f032d93F642f64180aa3 export CHAIN=hardhat # Start local hardhat node trap 'kill $(lsof -t -i:8545)' EXIT npx hardhat node &>/dev/null & sleep 10 # Init submodule #git submodule update --init # Install deps and build cd old #npm install npx hardhat compile # Deploy the old version npx hardhat run scripts/deploy.js --network custom # Move to project root cd .. # Run upgrade test suite npx hardhat test --network custom --config hardhat-upgrade.config.js --bail ================================================ FILE: test/_helpers/auction.js ================================================ import { RocketAuctionManager } from '../_utils/artifacts'; // Get lot start/end blocks export async function getLotStartBlock(lotIndex) { const rocketAuctionManager = await RocketAuctionManager.deployed(); return rocketAuctionManager.getLotStartBlock(lotIndex); } export async function getLotEndBlock(lotIndex) { const rocketAuctionManager = await RocketAuctionManager.deployed(); return rocketAuctionManager.getLotEndBlock(lotIndex); } // Get lot price at a block export async function getLotPriceAtBlock(lotIndex, block) { const rocketAuctionManager = await RocketAuctionManager.deployed(); return rocketAuctionManager.getLotPriceAtBlock(lotIndex, block); } // Create a new lot for auction export async function auctionCreateLot(txOptions) { const rocketAuctionManager = await RocketAuctionManager.deployed(); await rocketAuctionManager.connect(txOptions.from).createLot(txOptions); } // Place a bid on a lot export async function auctionPlaceBid(lotIndex, txOptions) { const rocketAuctionManager = await RocketAuctionManager.deployed(); await rocketAuctionManager.connect(txOptions.from).placeBid(lotIndex, txOptions); } ================================================ FILE: test/_helpers/beaconchain.js ================================================ const hre = require('hardhat'); const ethers = hre.ethers; export const beaconGenesisTime = 1606824023; export const secondsPerSlot = 12; export const slotsPerEpoch = 32; export async function getSlotForBlock(blockNumber = null) { const latestBlock = await ethers.provider.getBlock(blockNumber || 'latest'); const currentTime = latestBlock.timestamp; return Math.floor((currentTime - beaconGenesisTime) / secondsPerSlot); } export async function getCurrentEpoch() { const slotsPassed = await getSlotForBlock('latest'); return Math.floor(slotsPassed / slotsPerEpoch); } ================================================ FILE: test/_helpers/bigmath.js ================================================ export function BigMin(...args) { let smallest = args[0]; for (let i = 1; i < args.length; i++) { if (args[i] < smallest) { smallest = args[i]; } } return smallest; } export function BigSqrt(value) { if (value < 0n) { throw 'negative number'; } if (value < 2n) { return value; } function newtonIteration(n, x0) { const x1 = ((n / x0) + x0) >> 1n; if (x0 === x1 || x0 === (x1 - 1n)) { return x0; } return newtonIteration(n, x1); } return newtonIteration(value, 1n); } ================================================ FILE: test/_helpers/bn.js ================================================ import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; const _assertBN = { equal: function (actual, expected, message) { assert.strictEqual(actual, BigInt(expected), message); }, almostEqual: function (actual, expected, epsilon, message) { if (actual > expected) { assert.equal((actual - expected) < epsilon, true, message); } else { assert.equal((expected - actual) < epsilon, true, message); } }, notEqual: function (actual, expected, message) { assert.notEqual(actual, BigInt(expected), message); }, isBelow: function (actual, n, message) { assert.equal(actual < BigInt(n), true, message); }, isAbove: function (actual, n, message) { assert.equal(actual > BigInt(n), true, message); }, isAtMost: function (actual, n, message) { assert.equal(actual <= BigInt(n), true, message); }, isAtLeast: function (actual, n, message) { assert.equal(actual >= BigInt(n), true, message); }, isZero: function (actual, message) { assert.strictEqual(actual, 0n, message); }, } export function injectBNHelpers() { String.prototype.__defineGetter__('ether', function () { return ethers.parseUnits(this, 'ether'); }); String.prototype.__defineGetter__('gwei', function () { return ethers.parseUnits(this, 'gwei'); }); String.prototype.__defineGetter__('BN', function () { return BigInt(this); }); Number.prototype.__defineGetter__('BN', function () { return BigInt(this); }); Number.prototype.__defineGetter__('ether', function () { return ethers.parseUnits(this.toString(), 'ether'); }); Number.prototype.__defineGetter__('gwei', function () { return ethers.parseUnits(this.toString(), 'gwei'); }); } export const assertBN = _assertBN; ================================================ FILE: test/_helpers/console.js ================================================ // Calls func and suppresses all output to stdout and stderr unless and error occurs export async function suppressLog(func) { let stdout = process.stdout.write; let stderr = process.stderr.write; let logs = []; process.stdout.write = function() { logs.push(['stdout', arguments]); }; process.stderr.write = function() { logs.push(['stderr', arguments]); }; let result; try { result = await func(); } catch (e) { process.stdout.write = stdout; process.stderr.write = stderr; for (const log of logs) { process[log[0]].write.apply(process[log[0]], log[1]); } throw e; } process.stdout.write = stdout; process.stderr.write = stderr; return result; } ================================================ FILE: test/_helpers/dao.js ================================================ import { RocketDAONodeTrusted, RocketDAONodeTrustedActions, RocketDAONodeTrustedSettingsMembers, RocketDAOProtocolSettingsProposals, RocketDAOProtocolSettingsSecurity, RocketDAOProtocolVerifier, } from '../_utils/artifacts'; import { approveRPL, mintRPL } from './tokens'; export async function mintRPLBond(owner, node) { // Load contracts const [ rocketDAONodeTrustedActions, rocketDAONodeTrustedSettings, ] = await Promise.all([ RocketDAONodeTrustedActions.deployed(), RocketDAONodeTrustedSettingsMembers.deployed(), ]); // Get RPL bond amount const bondAmount = await rocketDAONodeTrustedSettings.getRPLBond.call(); // Mint RPL amount and approve DAO node contract to spend await mintRPL(owner, node, bondAmount); await approveRPL(rocketDAONodeTrustedActions.address, bondAmount, { from: node }); } export async function bootstrapMember(address, id, url, txOptions) { const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); await rocketDAONodeTrusted.bootstrapMember(id, url, address, txOptions); } export async function memberJoin(txOptions) { const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); await rocketDAONodeTrustedActions.actionJoin(txOptions); } export async function getDaoProtocolChallenge(proposalID, challengeID) { // Load contracts const rocketDAOProtocolVerifier = await RocketDAOProtocolVerifier.deployed(); return rocketDAOProtocolVerifier.getChallenge(proposalID, challengeID); } export async function getDaoProtocolVotePhase1Time() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return Number(await rocketDAOProtocolSettingsProposals.getVotePhase1Time()); } export async function getDaoProtocolVotePhase2Time() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return Number(await rocketDAOProtocolSettingsProposals.getVotePhase2Time()); } export async function getDaoProtocolVoteDelayTime() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return Number(await rocketDAOProtocolSettingsProposals.getVoteDelayTime()); } export async function getDaoProtocolSecurityLeaveTime() { // Load contracts const rocketDAOProtocolSettingsSecurity = await RocketDAOProtocolSettingsSecurity.deployed(); return Number(await rocketDAOProtocolSettingsSecurity.getLeaveTime()); } export async function getDaoProtocolDepthPerRound() { // Load contracts const rocketDAOProtocolVerifier = await RocketDAOProtocolVerifier.deployed(); return Number(await rocketDAOProtocolVerifier.getDepthPerRound()); } export async function getDaoProtocolChallengeBond() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return await rocketDAOProtocolSettingsProposals.getChallengeBond(); } export async function getDaoProtocolProposalBond() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return await rocketDAOProtocolSettingsProposals.getProposalBond(); } export async function getDaoProtocolChallengePeriod() { // Load contracts const rocketDAOProtocolSettingsProposals = await RocketDAOProtocolSettingsProposals.deployed(); return Number(await rocketDAOProtocolSettingsProposals.getChallengePeriod()); } ================================================ FILE: test/_helpers/defaults.js ================================================ import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsNode } from '../_utils/artifacts'; const hre = require('hardhat'); const ethers = hre.ethers; export async function setDefaultParameters() { const [guardian] = await ethers.getSigners(); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '1000'.ether, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.registration.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.deposit.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.submit.withdrawable.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.05'.ether, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.1'.ether, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.2'.ether, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.demand.range', '1000'.ether, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsInflation, 'rpl.inflation.interval.start', Math.floor(new Date().getTime() / 1000) + (60 * 60 * 24 * 14), { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.bond.reduction.enabled', true, { from: guardian }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.vacant.minipools.enabled', true, { from: guardian }); } ================================================ FILE: test/_helpers/deployer.js ================================================ import { artifacts } from '../_utils/artifacts'; import hre from 'hardhat'; import { injectBNHelpers } from './bn'; const fs = require('fs'); const pako = require('pako'); const ethers = hre.ethers; injectBNHelpers(); function compressABI(abi) { return Buffer.from(pako.deflate(JSON.stringify(abi))).toString('base64'); } function loadABI(abiFilePath) { return JSON.parse(fs.readFileSync(abiFilePath)); } function formatConstructorArgs(args) { return JSON.stringify(args, (key, value) => typeof value === 'bigint' ? value.toString() : value, ); } const defaultOpts = { protocolVersion: '1.4', initialRevenueSplit: ['0.05'.ether, '0.09'.ether, '0'.ether], depositAddress: null, fixedSupplyTokenAddress: null, deployThirdParty: false, genesisBlockTimestamp: 1606824023n, genesisValidatorRoot: '0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95', secondsPerSlot: 12n, slotsPerHistoricalRoot: 8192n, beaconRootsHistoryBufferLength: 8191n, beaconRoots: '0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02', logging: true, forkSlots: [ 74240n * 32n, // Altair 144896n * 32n, // Bellatrix 194048n * 32n, // Capella 269568n * 32n, // Deneb 364032n * 32n, // Electra ] }; const contractNameMap = { rocketVault: 'RocketVault', rocketTokenRPL: 'RocketTokenRPL', rocketTokenRPLFixedSupply: 'RocketTokenDummyRPL', rocketTokenRETH: 'RocketTokenRETH', rocketAuctionManager: 'RocketAuctionManager', rocketDepositPool: 'RocketDepositPool', rocketMinipoolDelegate: 'RocketMinipoolDelegate', rocketMinipoolManager: 'RocketMinipoolManager', rocketMinipoolQueue: 'RocketMinipoolQueue', rocketMinipoolPenalty: 'RocketMinipoolPenalty', rocketNetworkBalances: 'RocketNetworkBalances', rocketNetworkFees: 'RocketNetworkFees', rocketNetworkPrices: 'RocketNetworkPrices', rocketNetworkPenalties: 'RocketNetworkPenalties', rocketRewardsPool: 'RocketRewardsPool', rocketClaimDAO: 'RocketClaimDAO', rocketNodeDeposit: 'RocketNodeDeposit', rocketNodeManager: 'RocketNodeManager', rocketNodeStaking: 'RocketNodeStaking', rocketDAOProposal: 'RocketDAOProposal', rocketDAONodeTrusted: 'RocketDAONodeTrusted', rocketDAONodeTrustedProposals: 'RocketDAONodeTrustedProposals', rocketDAONodeTrustedActions: 'RocketDAONodeTrustedActions', rocketDAONodeTrustedUpgrade: 'RocketDAONodeTrustedUpgrade', rocketDAONodeTrustedSettingsMembers: 'RocketDAONodeTrustedSettingsMembers', rocketDAONodeTrustedSettingsProposals: 'RocketDAONodeTrustedSettingsProposals', rocketDAONodeTrustedSettingsMinipool: 'RocketDAONodeTrustedSettingsMinipool', rocketDAOProtocol: 'RocketDAOProtocol', rocketDAOProtocolProposals: 'RocketDAOProtocolProposals', rocketDAOProtocolActions: 'RocketDAOProtocolActions', rocketDAOProtocolSettingsInflation: 'RocketDAOProtocolSettingsInflation', rocketDAOProtocolSettingsRewards: 'RocketDAOProtocolSettingsRewards', rocketDAOProtocolSettingsAuction: 'RocketDAOProtocolSettingsAuction', rocketDAOProtocolSettingsNode: 'RocketDAOProtocolSettingsNode', rocketDAOProtocolSettingsNetwork: 'RocketDAOProtocolSettingsNetwork', rocketDAOProtocolSettingsDeposit: 'RocketDAOProtocolSettingsDeposit', rocketDAOProtocolSettingsMinipool: 'RocketDAOProtocolSettingsMinipool', rocketMerkleDistributorMainnet: 'RocketMerkleDistributorMainnet', rocketDAONodeTrustedSettingsRewards: 'RocketDAONodeTrustedSettingsRewards', rocketSmoothingPool: 'RocketSmoothingPool', rocketNodeDistributorFactory: 'RocketNodeDistributorFactory', rocketNodeDistributorDelegate: 'RocketNodeDistributorDelegate', rocketMinipoolFactory: 'RocketMinipoolFactory', rocketMinipoolBase: 'RocketMinipoolBase', rocketMinipoolBondReducer: 'RocketMinipoolBondReducer', rocketNetworkSnapshots: 'RocketNetworkSnapshots', rocketNetworkSnapshotsTime: 'RocketNetworkSnapshotsTime', rocketNetworkVoting: 'RocketNetworkVoting', rocketDAOProtocolSettingsProposals: 'RocketDAOProtocolSettingsProposals', rocketDAOProtocolVerifier: 'RocketDAOProtocolVerifier', rocketDAOSecurity: 'RocketDAOSecurity', rocketDAOSecurityActions: 'RocketDAOSecurityActions', rocketDAOSecurityProposals: 'RocketDAOSecurityProposals', rocketDAOProtocolSettingsSecurity: 'RocketDAOProtocolSettingsSecurity', rocketDAOProtocolProposal: 'RocketDAOProtocolProposal', rocketMegapoolFactory: 'RocketMegapoolFactory', rocketMegapoolProxy: 'RocketMegapoolProxy', rocketMegapoolManager: 'RocketMegapoolManager', rocketMegapoolDelegate: 'RocketMegapoolDelegate', rocketMegapoolPenalties: 'RocketMegapoolPenalties', rocketNetworkRevenues: 'RocketNetworkRevenues', rocketDAOProtocolSettingsMegapool: 'RocketDAOProtocolSettingsMegapool', rocketDAOSecurityUpgrade: 'RocketDAOSecurityUpgrade', addressQueueStorage: 'AddressQueueStorage', addressSetStorage: 'AddressSetStorage', beaconStateVerifier: 'BeaconStateVerifier', linkedListStorage: 'LinkedListStorage', }; export class RocketPoolDeployer { signer = null; rocketStorageInstance = null; contractPlan = {}; deployedContracts = {}; skippedContracts = []; logDepth = 0; buildInfos = {}; deployBlock = null; stages = []; stageTxs = []; constructor(signer, opts = {}) { this.signer = signer; opts = { ...defaultOpts, ...opts }; if (!opts.logging) { this.log = () => {}; } // Setup default contract deployment plan this.contractPlan['rocketStorage'] = { constructorArgs: [], artifact: artifacts.require('RocketStorage'), }; for (const contract in contractNameMap) { this.contractPlan[contract] = { constructorArgs: () => this.defaultConstructorArgs(), artifact: artifacts.require(contractNameMap[contract]), }; } // Override constructor args on certain contracts this.contractPlan['rocketTokenRPL'].constructorArgs = () => [this.rocketStorageInstance.target, this.deployedContracts['rocketTokenRPLFixedSupply'].address]; this.contractPlan['rocketMinipoolDelegate'].constructorArgs = []; this.contractPlan['rocketNodeDistributorDelegate'].constructorArgs = []; this.contractPlan['rocketMinipoolBase'].constructorArgs = []; this.contractPlan['beaconStateVerifier'].constructorArgs = () => [this.rocketStorageInstance.target, opts.slotsPerHistoricalRoot, opts.forkSlots, opts.beaconRoots, opts.genesisBlockTimestamp, opts.genesisValidatorRoot]; this.contractPlan['rocketMegapoolDelegate'].constructorArgs = () => [this.rocketStorageInstance.target]; // Setup deployment this.addStage('Deploy storage', 0, [ async () => this.deployNetworkContract('rocketStorage'), async () => this.setString('protocol.version', opts.protocolVersion), async () => this.setUint('deploy.block', this.deployBlock), ], ); if (opts.depositAddress === null) { this.addStage('Deploy deposit contract', 10, [ async () => this.deployDepositContract(), ], ); } else { const abi = loadABI('./contracts/contract/casper/compiled/Deposit.abi'); this.addStage('Setup deposit contract', 10, [ async () => this.setNetworkContractAddress('casperDeposit', opts.depositAddress), async () => this.setNetworkContractAbi('casperDeposit', abi), ], ); } if (opts.fixedSupplyTokenAddress === null) { // Has to be deployed before RPL token as it's used in constructor this.addStage('Deploy dummy RPL fixed supply token', 20, [ async () => this.deployNetworkContract('rocketTokenRPLFixedSupply'), ], ); } else { this.addStage('Setup RPL fixed supply', 20, [ async () => this.setNetworkContractAddress('rocketTokenRPLFixedSupply', opts.fixedSupplyTokenAddress), async () => this.setNetworkContractAbi('rocketTokenRPLFixedSupply', artifacts.require('rocketTokenRPLFixedSupply').abi), ], ); // No need to deploy this anymore this.skippedContracts.push('rocketTokenRPLFixedSupply'); } this.addStage('Deploy immutable contracts', 30, [ async () => this.deployNetworkContract('rocketVault'), async () => this.deployNetworkContract('rocketTokenRETH'), async () => this.deployNetworkContract('rocketTokenRPL'), ], ); this.addStage('Deploy remaining network contracts', 40, [ async () => this.deployRemainingContracts(), ], ); this.addStage('Add combined minipool and megapool ABI', 50, [ async () => this.setNetworkContractAbi('rocketMinipool', compressABI(this.getMinipoolAbi())), async () => this.setNetworkContractAbi('rocketMegapool', compressABI(this.getMegapoolAbi())), ], ); this.addStage('Initialise contracts', 60, [ async () => await this.deployedContracts['rocketMegapoolFactory'].instance.initialise(), async () => await this.deployedContracts['rocketNetworkRevenues'].instance.initialise(...opts.initialRevenueSplit), async () => await this.deployedContracts['rocketMerkleDistributorMainnet'].instance.initialise(), ], ); this.addStage('Lock storage', 100, [ async () => this.setDeploymentStatus(), ], ); if (opts.deployThirdParty) { this.addStage('Deploy third party contracts', 200, [ async () => this.deployThirdPartyContracts(), ], ); } } log(string = '\n', color = 'gray') { let colorCodes = { 'white': 0, 'gray': 37, 'red': 31, 'blue': 34, 'green': 32, }; console.log('%s\x1b[%sm%s\x1b[0m', ''.padEnd(this.logDepth, ' '), colorCodes[color], string); } addStage(name, priority, steps) { this.stages.push({ name, priority, steps, }); } defaultConstructorArgs() { return [this.rocketStorageInstance.target]; } getMinipoolAbi() { // Construct ABI for rocketMinipool const rocketMinipoolAbi = [] .concat(artifacts.require('RocketMinipoolDelegate').abi) .concat(artifacts.require('RocketMinipoolBase').abi) .filter(i => i.type !== 'fallback' && i.type !== 'receive'); rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'fallback' }); rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'receive' }); return rocketMinipoolAbi; } getMegapoolAbi() { // Construct ABI for rocketMegapool const rocketMegapoolAbi = [] .concat(artifacts.require('RocketMegapoolDelegate').abi) .concat(artifacts.require('RocketMegapoolProxy').abi) .filter(i => i.type !== 'fallback' && i.type !== 'receive'); rocketMegapoolAbi.push({ stateMutability: 'payable', type: 'fallback' }); rocketMegapoolAbi.push({ stateMutability: 'payable', type: 'receive' }); return rocketMegapoolAbi; } async setDeploymentStatus() { // Disable direct access to storage now this.log('- Locking down storage'); const tx = await this.rocketStorageInstance.setDeployedStatus(); this.stageTxs.push(tx); } async setString(name, value) { this.log('- Setting string `' + name + '` to ' + value, 'white'); const tx = await this.rocketStorageInstance.setString( ethers.solidityPackedKeccak256(['string'], [name]), value, ); this.stageTxs.push(tx); } async setUint(name, value) { this.log('- Setting uint `' + name + '` to ' + value, 'white'); const tx = await this.rocketStorageInstance.setUint( ethers.solidityPackedKeccak256(['string'], [name]), value, ); this.stageTxs.push(tx); } async deployDepositContract() { this.log('- Deploying deposit contract', 'white'); const abi = loadABI('./contracts/contract/casper/compiled/Deposit.abi'); const factory = new ethers.ContractFactory(abi, fs.readFileSync('./contracts/contract/casper/compiled/Deposit.bin').toString(), this.signer); const instance = await factory.deploy(); const address = instance.target; this.log(` - Deployed to ${address}`); await this.setNetworkContractAddress('casperDeposit', address); await this.setNetworkContractAbi('casperDeposit', abi); } async setNetworkContractAddress(name, address) { this.log(`- Setting address for "${name}" in storage to ${address}`); // Register the contract address as part of the network const tx1 = await this.rocketStorageInstance.setBool( ethers.solidityPackedKeccak256(['string', 'address'], ['contract.exists', address]), true, ); // Register the contract's name by address const tx2 = await this.rocketStorageInstance.setString( ethers.solidityPackedKeccak256(['string', 'address'], ['contract.name', address]), name, ); // Register the contract's address by name (rocketVault and rocketTokenRETH addresses already stored) const tx3 = await this.rocketStorageInstance.setAddress( ethers.solidityPackedKeccak256(['string', 'string'], ['contract.address', name]), address, ); this.stageTxs.push(tx1); this.stageTxs.push(tx2); this.stageTxs.push(tx3); } async setNetworkContractAbi(name, abi) { let compressedAbi = abi; if (Array.isArray(compressedAbi)) { compressedAbi = compressABI(abi); } this.log(`- Setting abi for "${name}" in storage to ${compressedAbi.substr(0, 40)}...`); // Compress and store the ABI by name const tx = await this.rocketStorageInstance.setString( ethers.solidityPackedKeccak256(['string', 'string'], ['contract.abi', name]), compressedAbi, ); this.stageTxs.push(tx); } async deployRemainingContracts() { for (const contract in this.contractPlan) { if (this.deployedContracts.hasOwnProperty(contract)) { this.log(`- Skipping already deployed ${contract}`, 'red'); continue; } if (this.skippedContracts.includes(contract)) { this.log(`- Skipping ${contract}`, 'red'); continue; } await this.deployNetworkContract(contract); } } async deployNetworkContract(name) { const plan = this.contractPlan[name]; if (!plan) { throw Error(`No contract deployment plan for ${name}`); } let artifact = plan.artifact; let abi = artifact.abi; this.log(`- Deploying "${name}"`, 'white'); let constructorArgs = typeof plan.constructorArgs === 'function' ? plan.constructorArgs() : plan.constructorArgs; this.logDepth += 2; this.log(`- Constructor args = ${formatConstructorArgs(constructorArgs)}`); // Deploy and log result const instance = await artifact.newImmediate(...constructorArgs); const rsTx = await instance.deploymentTransaction(); const address = instance.target; this.log(`- Deployed to ${address} @ ${rsTx.hash}`); // Encode the constructor args const iface = new ethers.Interface(abi); const encodedConstructorArgs = iface.encodeDeploy(constructorArgs); // Special case for rocketStorage as it's used for all value setting if (name === 'rocketStorage') { this.rocketStorageInstance = instance; const receipt = await rsTx.wait(); this.deployBlock = receipt.blockNumber; } else { this.stageTxs.push(rsTx); } await this.setNetworkContractAddress(name, address); await this.setNetworkContractAbi(name, abi); // Add to deployed contracts this.deployedContracts[name] = { artifact: artifact, constructorArgs: encodedConstructorArgs, abi: abi, address: address, instance: instance, }; this.logDepth -= 2; } async bootstrapProtocolDAOSetting(contractName, settingPath, value) { const rocketDAOProtocol = this.deployedContracts['rocketDAOProtocol'].instance; if (ethers.isAddress(value)) { this.log(`- Bootstrap pDAO setting address \`${settingPath}\` = "${value}" on \`${contractName}\``, 'white'); const tx = await rocketDAOProtocol.bootstrapSettingAddress(contractName, settingPath, value); this.stageTxs.push(tx); } else { if (typeof (value) == 'number' || typeof (value) == 'string' || typeof (value) == 'bigint') { this.log(`- Bootstrap pDAO setting uint \`${settingPath}\` = ${value} on \`${contractName}\``, 'white'); const tx = await rocketDAOProtocol.bootstrapSettingUint(contractName, settingPath, value); this.stageTxs.push(tx); } else if (typeof (value) == 'boolean') { this.log(`- Bootstrap pDAO setting bool \`${settingPath}\` = ${value} on \`${contractName}\``, 'white'); const tx = await rocketDAOProtocol.bootstrapSettingBool(contractName, settingPath, value); this.stageTxs.push(tx); } } } async bootstrapProtocolDAOClaimers(trustedNodePerc, protocolPerc, nodePerc) { const rocketDAOProtocol = this.deployedContracts['rocketDAOProtocol'].instance; this.log(`- Bootstrap pDAO setting claimers: oDAO = ${ethers.formatEther(trustedNodePerc * 100n)}%, protocol = ${ethers.formatEther(protocolPerc * 100n)}%, node = ${ethers.formatEther(nodePerc * 100n)}% `, 'white'); const tx = await rocketDAOProtocol.bootstrapSettingClaimers(trustedNodePerc, protocolPerc, nodePerc); this.stageTxs.push(tx); } async deploy() { this.log(`Deploying RocketPool`, 'green'); // Sort stages by priority this.stages.sort((a, b) => a.priority - b.priority); // Iterate over stages and execute steps for (let l = 0; l < this.stages.length; ++l) { const stage = this.stages[l]; this.log(`# ${stage.name}`, 'blue'); this.logDepth += 2; // Clear txs for this stage this.stageTxs = []; // Iterate over steps and execute for (let i = 0; i < stage.steps.length; ++i) { // Execute the step await stage.steps[i](); } // Wait for all txs to complete this.log(`Waiting for ${this.stageTxs.length} txs to be mined`) await Promise.all(this.stageTxs.map(async tx => tx.wait())) this.logDepth -= 2; this.log(); } return this.deployedContracts; } async deployThirdPartyContract(artifactName, constructorArgs = []) { const artifact = artifacts.require(artifactName); // Deploy and log result this.log('- Deploying ' + artifact.contractName, 'white'); const instance = await artifact.newImmediate(...constructorArgs); const rsTx = await instance.deploymentTransaction(); const address = instance.target; this.log(`- Deployed to ${address} @ ${rsTx.hash}`); this.stageTxs.push(rsTx); } async deployThirdPartyContracts() { await this.deployThirdPartyContract('EthBalanceChecker'); await this.deployThirdPartyContract('Multicall2'); await this.deployThirdPartyContract('RocketSignerRegistry'); await this.deployThirdPartyContract('UniswapOracleMock'); } } ================================================ FILE: test/_helpers/deployment.js ================================================ import { artifacts } from '../_utils/artifacts'; import { RocketPoolDeployer } from './deployer'; const hre = require('hardhat'); const ethers = hre.ethers; const revertOnTransfer = artifacts.require('RevertOnTransfer'); // Deploy Rocket Pool with mocks and helpers used in testing export async function deployRocketPool() { const [signer] = await ethers.getSigners(); const deployer = new RocketPoolDeployer(signer, { logging: false }); deployer.contractPlan['beaconStateVerifier'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('BeaconStateVerifierMock'), } // Add helper contracts to deployment deployer.contractPlan['linkedListStorageHelper'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('LinkedListStorageHelper'), }; deployer.contractPlan['storageHelper'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('StorageHelper'), }; deployer.contractPlan['megapoolUpgradeHelper'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('MegapoolUpgradeHelper'), }; deployer.contractPlan['stakeHelper'] = { constructorArgs: () => deployer.defaultConstructorArgs(), artifact: artifacts.require('StakeHelper'), }; await deployer.deploy(); // Deploy other utilities used in tests that aren't network contracts let revertOnTransferInstance = await revertOnTransfer.new(); revertOnTransfer.setAsDeployed(revertOnTransferInstance); } ================================================ FILE: test/_helpers/deposit.js ================================================ import { RocketDepositPool } from '../_utils/artifacts'; // Get the deposit pool excess ETH balance export async function getDepositExcessBalance() { const rocketDepositPool = await RocketDepositPool.deployed(); return rocketDepositPool.getExcessBalance.call(); } // Make a deposit export async function userDeposit(txOptions) { const rocketDepositPool = await RocketDepositPool.deployed(); await rocketDepositPool.connect(txOptions.from).deposit(txOptions); } // Assign deposits export async function assignDeposits(txOptions) { const rocketDepositPool = await RocketDepositPool.deployed(); await rocketDepositPool.assignDeposits(txOptions); } ================================================ FILE: test/_helpers/invariants.js ================================================ const { RocketNodeManager, RocketMinipoolManager, RocketMinipoolDelegate } = require('../../test/_utils/artifacts'); const { assertBN } = require('./bn'); const assert = require('assert'); const { getMegapoolForNodeAddress } = require('./megapool'); const { RocketNodeStaking, RocketDepositPool } = require('../_utils/artifacts'); async function checkInvariants() { const nodeAddresses = await getNodeAddresses(); for (const nodeAddress of nodeAddresses) { const minipools = await getMinipoolsByNode(nodeAddress); await checkNodeInvariants(nodeAddress, minipools); } await checkMegapoolInvariants() } async function checkMegapoolInvariants() { // Check deposit.pool.node.balance invariant const nodeAddresses = await getNodeAddresses(); let totalNodeQueuedBond = 0n for (const nodeAddress of nodeAddresses) { const megapool = await getMegapoolForNodeAddress(nodeAddress); // Sum queued bond if (megapool) { const nodeQueuedBond = await megapool.getNodeQueuedBond(); totalNodeQueuedBond += nodeQueuedBond; } // Check ETH matched and ETH provided match megapool values if (megapool) { const rocketNodeStaking = await RocketNodeStaking.deployed(); const ethBorrowed = await rocketNodeStaking.getNodeMegapoolETHBorrowed(nodeAddress); const ethBonded = await rocketNodeStaking.getNodeMegapoolETHBonded(nodeAddress); const nodeBond = (await megapool.getNodeBond()) + (await megapool.getNodeQueuedBond()); const userCapital = (await megapool.getUserCapital()) + (await megapool.getUserQueuedCapital()); assertBN.equal(ethBorrowed, userCapital, 'ETH borrowed did not match user capital'); assertBN.equal(ethBonded, nodeBond, 'ETH bonded did not match node bond'); } } // Check sum of queued bond equals the node balance const rocketDepositPool = await RocketDepositPool.deployed() const nodeBalance = await rocketDepositPool.getNodeBalance(); assertBN.equal(nodeBalance, totalNodeQueuedBond, "Node balance does not match") } async function getNodeAddresses() { const rocketNodeManager = await RocketNodeManager.deployed(); return await rocketNodeManager.getNodeAddresses(0, 1000); } async function getMinipoolDetails(address) { const minipool = await RocketMinipoolDelegate.at(address); const [status, finalised, nodeFee, userDepositBalance, nodeDepositBalance] = await Promise.all([ minipool.getStatus(), minipool.getFinalised(), minipool.getNodeFee(), minipool.getUserDepositBalance(), minipool.getNodeDepositBalance(), ]); return { status: status.toString(), finalised, nodeFee, userDepositBalance, nodeDepositBalance, }; } async function getMinipoolsByNode(nodeAddress) { const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const count = await rocketMinipoolManager.getNodeMinipoolCount(nodeAddress); const minipools = []; for (let i = 0; i < count; i++) { const address = await rocketMinipoolManager.getNodeMinipoolAt(nodeAddress, i); minipools.push(await getMinipoolDetails(address)); } return minipools; } async function checkNodeInvariants(nodeAddress, minipools) { const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const rocketNodeManager = await RocketNodeManager.deployed(); const depositSizes = ['8'.ether, '16'.ether]; // Filter "staking" minipools const stakingMinipools = minipools.filter(minipool => minipool.status === '2' && minipool.finalised === false); // Check overall counts const [expectedActive, expectedFinalised, expectedStaking] = await Promise.all([ rocketMinipoolManager.getNodeActiveMinipoolCount(nodeAddress), rocketMinipoolManager.getNodeFinalisedMinipoolCount(nodeAddress), rocketMinipoolManager.getNodeStakingMinipoolCount(nodeAddress), ]); const actualActive = minipools.filter(minipool => minipool.finalised !== true).length; const actualFinalised = minipools.length - actualActive; const actualStaking = stakingMinipools.length; assert.equal(actualActive, Number(expectedActive), 'Active minipool count invariant broken'); assert.equal(actualFinalised, Number(expectedFinalised), 'Finalised minipool count invariant broken'); assert.equal(actualStaking, Number(expectedStaking), 'Staking minipool count invariant broken'); // Check deposit size counts const countBySize = await Promise.all(depositSizes.map(depositSize => rocketMinipoolManager.getNodeStakingMinipoolCountBySize(nodeAddress, depositSize))); for (let i = 0; i < depositSizes.length; i++) { const depositSize = depositSizes[i]; const actualCount = Number(countBySize[i]); const expectedCount = stakingMinipools.filter(minipool => minipool.nodeDepositBalance === depositSize).length; assert.equal(actualCount, expectedCount, 'Deposit size specific staking minipool count invariant broken'); } // Check weighted average node fee const expectedFee = weightedAverage( stakingMinipools.map(minipool => minipool.nodeFee), stakingMinipools.map(minipool => minipool.userDepositBalance), ); const actualFee = await rocketNodeManager.getAverageNodeFee(nodeAddress); assertBN.equal(actualFee, expectedFee, 'Average node fee invariant broken'); } function weightedAverage(nums, weights) { if (nums.length === 0) { return 0n; } const [sum, weightSum] = weights.reduce( (acc, w, i) => { acc[0] = acc[0] + (nums[i] * w); acc[1] = acc[1] + w; return acc; }, [0n, 0n], ); return sum / weightSum; } module.exports = { checkInvariants, checkMegapoolInvariants }; ================================================ FILE: test/_helpers/megapool.js ================================================ import { assertBN } from './bn'; import * as assert from 'assert'; import { artifacts, LinkedListStorage, RocketDAOProtocolSettingsDeposit, RocketDepositPool, RocketMegapoolDelegate, RocketMegapoolFactory, RocketMegapoolManager, RocketNodeDeposit, RocketNodeManager, RocketNodeStaking, } from '../_utils/artifacts'; import { getDepositDataRoot, getValidatorPubkey, getValidatorSignature } from '../_utils/beacon'; const hre = require('hardhat'); const ethers = hre.ethers; const milliToWei = 1000000000000000n; export async function getValidatorInfo(megapool, index) { const [validatorInfo, pubkey] = await megapool.getValidatorInfoAndPubkey(index); return { pubkey, lastAssignmentTime: validatorInfo[0], lastRequestedValue: validatorInfo[1], lastRequestedBond: validatorInfo[2], depositValue: validatorInfo[3], staked: validatorInfo[4], exited: validatorInfo[5], inQueue: validatorInfo[6], inPrestake: validatorInfo[7], expressUsed: validatorInfo[8], dissolved: validatorInfo[9], exiting: validatorInfo[10], locked: validatorInfo[11], exitBalance: validatorInfo[12], lockedSlot: validatorInfo[13], }; } export async function deployMegapool(txOptions) { const rocketNodeManager = await RocketNodeManager.deployed(); const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); // Will revert if megapool already exists await rocketNodeManager.connect(txOptions.from).deployMegapool(); // Check `getMegapoolDeployed` returns true const existsAfter = await rocketMegapoolFactory.getMegapoolDeployed(txOptions.from.address); assert.equal(existsAfter, true, 'Megapool was not created'); } export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket = false, creditAmount = '0'.ether) { const [ rocketNodeDeposit, rocketNodeManager, rocketNodeStaking, rocketMegapoolFactory, rocketDepositPool, rocketDAOProtocolSettingsDeposit, rocketMegapoolManager, linkedListStorage, ] = await Promise.all([ RocketNodeDeposit.deployed(), RocketNodeManager.deployed(), RocketNodeStaking.deployed(), RocketMegapoolFactory.deployed(), RocketDepositPool.deployed(), RocketDAOProtocolSettingsDeposit.deployed(), RocketMegapoolManager.deployed(), LinkedListStorage.deployed(), ]); // Construct deposit data for prestake let withdrawalCredentials = await getMegapoolWithdrawalCredentials(node.address); let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); let usingCredit = creditAmount > 0n; const queueIndex = await rocketDepositPool.getQueueIndex(); const expressQueueRate = await rocketDAOProtocolSettingsDeposit.getExpressQueueRate(); async function getData() { let data = await Promise.all([ rocketMegapoolFactory.getMegapoolDeployed(node.address), rocketNodeManager.getExpressTicketCount(node.address), rocketMegapoolManager.getValidatorCount(), rocketDepositPool.getExpressQueueLength(), rocketDepositPool.getStandardQueueLength(), rocketDepositPool.getNodeBalance(), rocketNodeStaking.getNodeETHBorrowed(node.address), rocketNodeStaking.getNodeETHBonded(node.address), rocketNodeStaking.getNodeMegapoolETHBorrowed(node.address), rocketNodeStaking.getNodeMegapoolETHBonded(node.address), rocketDepositPool.getMinipoolQueueLength(), ]).then( ([deployed, numExpressTickets, numGlobalValidators, expressQueueLength, standardQueueLength, nodeBalance, nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, minipoolQueueLength]) => ({ deployed, numExpressTickets, numGlobalValidators, expressQueueLength, standardQueueLength, nodeBalance, nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, minipoolQueueLength, numValidators: 0n, assignedValue: 0n, nodeBond: 0n, nodeQueuedCapital: 0n, userCapital: 0n, userQueuedCapital: 0n, }), ); if (data.deployed) { const megapool = (await getMegapoolForNode(node)); data.numValidators = await megapool.getValidatorCount(); data.assignedValue = await megapool.getAssignedValue(); data.nodeBond = await megapool.getNodeBond(); data.nodeQueuedCapital = await megapool.getNodeQueuedBond(); data.userCapital = await megapool.getUserCapital(); data.userQueuedCapital = await megapool.getUserQueuedCapital(); } return data; } const megapool = await getMegapoolForNode(node); const data1 = await getData(); const nextAssignmentIsExpress = (queueIndex % (expressQueueRate + 1n) !== expressQueueRate) && ( (data1.expressQueueLength === 0n && useExpressTicket) || (data1.expressQueueLength !== 0n) ); const assignmentsEnabled = await rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(); const depositPoolCapacity = await rocketDepositPool.getBalance() + bondAmount; const expressQueueNamespace = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.express']); const standardQueueNamespace = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.standard']); const queueLength = await linkedListStorage.getLength(nextAssignmentIsExpress ? expressQueueNamespace : standardQueueNamespace); let expectedNodeBalanceChange = bondAmount; let expectedUserQueuedCapitalChange = '32'.ether - bondAmount; let expectedNodeQueuedBondChange = bondAmount; let expectSelfAssignment = false; let expectImmediateAssignment = false; let expectStandardQueueChange = useExpressTicket ? 0n : 1n; let expectExpressQueueChange = useExpressTicket ? 1n : 0n; let expectExpressTicketsChange = useExpressTicket ? -1n : 0n; let amountRequired = '32'.ether; const minipoolInQueue = data1.minipoolQueueLength > 0n; if (assignmentsEnabled) { if (minipoolInQueue) { if (depositPoolCapacity >= amountRequired) { expectedNodeBalanceChange -= '16'.ether - '1'.ether; } } else if (queueLength > 0n) { const queueHead = await linkedListStorage.peekItem(nextAssignmentIsExpress ? expressQueueNamespace : standardQueueNamespace); if (depositPoolCapacity >= amountRequired) { expectedNodeBalanceChange -= queueHead[2] * milliToWei; if (nextAssignmentIsExpress) { expectExpressQueueChange -= 1n; } else { expectStandardQueueChange -= 1n; } if (queueHead[0] === megapool.target) { expectedUserQueuedCapitalChange -= '32'.ether - (queueHead[2] * milliToWei); expectedNodeQueuedBondChange -= queueHead[2] * milliToWei; expectSelfAssignment = true; } } } else { if (depositPoolCapacity >= amountRequired) { expectSelfAssignment = true; expectImmediateAssignment = true; expectedUserQueuedCapitalChange = 0n; expectedNodeQueuedBondChange = 0n; expectedNodeBalanceChange = 0n; if (nextAssignmentIsExpress) { expectExpressQueueChange -= 1n; } else { expectStandardQueueChange -= 1n; } } } } if (!usingCredit) { const tx = await rocketNodeDeposit.connect(node).deposit(bondAmount, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, { value: bondAmount }); await tx.wait(); } else { const tx = await rocketNodeDeposit.connect(node).depositWithCredit(bondAmount, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, { value: bondAmount - creditAmount }); await tx.wait(); } const data2 = await getData(); if (!data1.deployed) { assert.equal(data2.deployed, true, 'Megapool was not deployed'); } // Confirm state changes to node const numValidatorsDelta = data2.numValidators - data1.numValidators; const numGlobalValidatorsDelta = data2.numGlobalValidators - data1.numGlobalValidators; const numExpressTicketsDelta = data2.numExpressTickets - data1.numExpressTickets; const assignedValueDelta = data2.assignedValue - data1.assignedValue; const nodeBondDelta = data2.nodeBond - data1.nodeBond; const nodeQueuedBondDelta = data2.nodeQueuedCapital - data1.nodeQueuedCapital; const userCapitalDelta = data2.userCapital - data1.userCapital; const userQueuedCapitalDelta = data2.userQueuedCapital - data1.userQueuedCapital; const expressQueueLengthDelta = data2.expressQueueLength - data1.expressQueueLength; const standardQueueLengthDelta = data2.standardQueueLength - data1.standardQueueLength; const nodeBalanceDelta = data2.nodeBalance - data1.nodeBalance; const nodeEthBondedDelta = data2.nodeEthBonded - data1.nodeEthBonded; const nodeEthBorrowedDelta = data2.nodeEthBorrowed - data1.nodeEthBorrowed; assertBN.equal(nodeEthBondedDelta, bondAmount); assertBN.equal(nodeEthBorrowedDelta, '32'.ether - bondAmount); assertBN.equal(data2.nodeBond + data2.nodeQueuedCapital, data2.nodeMegapoolEthBonded); assertBN.equal(data2.userCapital + data2.userQueuedCapital, data2.nodeMegapoolEthBorrowed); assertBN.equal(numValidatorsDelta, 1n, 'Number of validators did not increase by 1'); assertBN.equal(numGlobalValidatorsDelta, 1n, 'Number of global validators did not increase by 1'); const expectAssignment = !minipoolInQueue && assignmentsEnabled && depositPoolCapacity >= amountRequired; const queueTop = await rocketDepositPool.getQueueTop(); let expectMegapoolAssignment = expectSelfAssignment || (expectAssignment && (queueTop[0] === megapool.target)); assertBN.equal(numExpressTicketsDelta, expectExpressTicketsChange, 'Did not consume express ticket'); assertBN.equal(expressQueueLengthDelta, expectExpressQueueChange, 'Express queue length incorrect'); assertBN.equal(standardQueueLengthDelta, expectStandardQueueChange, 'Standard queue length incorrect'); // Confirm state of new validator const validatorInfo = await getValidatorInfo(megapool, data1.numValidators); assertBN.equal(nodeBondDelta + nodeQueuedBondDelta, bondAmount, 'Incorrect node capital'); assertBN.equal(userCapitalDelta + userQueuedCapitalDelta, '32'.ether - bondAmount, 'Incorrect user capital'); const launchValue = '32'.ether; if (minipoolInQueue) { // Validator will never be assigned if a minipool exists in the queue as it is serviced first assert.equal(validatorInfo.inQueue, true, 'Incorrect validator status'); assert.equal(validatorInfo.inPrestake, false, 'Incorrect validator status'); assertBN.equal(assignedValueDelta, 0n, 'Incorrect assigned value'); } else if (expectImmediateAssignment) { assert.equal(validatorInfo.inQueue, false, 'Incorrect validator status'); assert.equal(validatorInfo.inPrestake, true, 'Incorrect validator status'); assertBN.equal(assignedValueDelta, '31'.ether, 'Incorrect assigned value'); } else { assert.equal(validatorInfo.inQueue, true, 'Incorrect validator status'); assert.equal(validatorInfo.inPrestake, false, 'Incorrect validator status'); } if (expectSelfAssignment) { assertBN.equal(assignedValueDelta, '31'.ether, 'Incorrect assigned value'); } else { assertBN.equal(assignedValueDelta, 0n, 'Incorrect assigned value'); } assertBN.equal(userQueuedCapitalDelta, expectedUserQueuedCapitalChange, 'Incorrect user queued capital'); assertBN.equal(nodeQueuedBondDelta, expectedNodeQueuedBondChange, 'Incorrect node queued bond'); assertBN.equal(nodeBalanceDelta, expectedNodeBalanceChange, 'Incorrect node balance value'); assertBN.equal(validatorInfo.lastRequestedValue, '32'.ether / milliToWei, 'Incorrect validator lastRequestedValue'); assertBN.equal(validatorInfo.lastRequestedBond, bondAmount / milliToWei, 'Incorrect validator lastRequestedBond'); assert.equal(validatorInfo.staked, false, 'Incorrect validator status'); assert.equal(validatorInfo.dissolved, false, 'Incorrect validator status'); assert.equal(validatorInfo.exited, false, 'Incorrect validator status'); assert.equal(validatorInfo.expressUsed, useExpressTicket, 'Incorrect validator express ticket usage'); assert.equal(validatorInfo.pubkey, '0x' + depositData.pubkey.toString('hex'), 'Incorrect validator pubkey'); } export async function nodeDepositMulti(node, deposits, creditAmount = 0n, value = null) { const [ rocketNodeDeposit, rocketNodeManager, rocketNodeStaking, rocketMegapoolFactory, rocketDepositPool, rocketDAOProtocolSettingsDeposit, rocketMegapoolManager, ] = await Promise.all([ RocketNodeDeposit.deployed(), RocketNodeManager.deployed(), RocketNodeStaking.deployed(), RocketMegapoolFactory.deployed(), RocketDepositPool.deployed(), RocketDAOProtocolSettingsDeposit.deployed(), RocketMegapoolManager.deployed(), ]); const depositParams = []; const withdrawalCredentials = await getMegapoolWithdrawalCredentials(node.address); let totalBond = 0n; let totalExpressTickets = 0; let pubkeys = []; for (let i = 0; i < deposits.length; i++) { let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); depositParams.push({ bondAmount: deposits[i].bondAmount, useExpressTicket: deposits[i].useExpressTicket, validatorPubkey: depositData.pubkey, validatorSignature: depositData.signature, depositDataRoot: depositDataRoot, }); pubkeys.push(depositData.pubkey); totalBond += deposits[i].bondAmount; totalExpressTickets += Number(deposits[i].useExpressTicket); } // Construct deposit data for prestake let msgValue = value === null ? (totalBond - creditAmount) : value; async function getData() { let data = await Promise.all([ rocketMegapoolFactory.getMegapoolDeployed(node.address), rocketNodeManager.getExpressTicketCount(node.address), rocketMegapoolManager.getValidatorCount(), rocketDepositPool.getExpressQueueLength(), rocketDepositPool.getStandardQueueLength(), rocketDepositPool.getNodeBalance(), rocketNodeStaking.getNodeETHBorrowed(node.address), rocketNodeStaking.getNodeETHBonded(node.address), rocketNodeStaking.getNodeMegapoolETHBorrowed(node.address), rocketNodeStaking.getNodeMegapoolETHBonded(node.address), ]).then( ([deployed, numExpressTickets, numGlobalValidators, expressQueueLength, standardQueueLength, nodeBalance, nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded]) => ({ deployed, numExpressTickets, numGlobalValidators, expressQueueLength, standardQueueLength, nodeBalance, nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, numValidators: 0n, assignedValue: 0n, nodeBond: 0n, userCapital: 0n, nodeQueuedBond: 0n, userQueuedCapital: 0n, }), ); if (data.deployed) { const megapool = (await getMegapoolForNode(node)); data.numValidators = await megapool.getValidatorCount(); data.assignedValue = await megapool.getAssignedValue(); data.nodeBond = await megapool.getNodeBond(); data.userCapital = await megapool.getUserCapital(); data.nodeQueuedBond = await megapool.getNodeQueuedBond(); data.userQueuedCapital = await megapool.getUserQueuedCapital(); } return data; } const data1 = await getData(); const assignmentsEnabled = await rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(); let expectedAssignments = 0; let expectedExpressAssignments = 0; let expectedNodeBalanceChange = 0n; if (assignmentsEnabled) { let depositPoolCapacity = await rocketDepositPool.getBalance(); for (let i = 0; i < deposits.length; ++i) { const amountRequired = '32'.ether - deposits[i].bondAmount; if (depositPoolCapacity >= amountRequired) { expectedAssignments += 1; depositPoolCapacity -= amountRequired; if (deposits[i].useExpressTicket) { expectedExpressAssignments += 1; } } else { expectedNodeBalanceChange += deposits[i].bondAmount; } } } const creditBefore = await rocketNodeDeposit.getNodeDepositCredit(node.address); const balanceBefore = await rocketNodeDeposit.getNodeEthBalance(node.address); const tx = await rocketNodeDeposit.connect(node).depositMulti(depositParams, { value: msgValue }); await tx.wait(); const creditAfter = await rocketNodeDeposit.getNodeDepositCredit(node.address); const balanceAfter = await rocketNodeDeposit.getNodeEthBalance(node.address); const creditAndBalanceDelta = (creditAfter + balanceAfter) - (creditBefore + balanceBefore); assertBN.equal(creditAndBalanceDelta, -creditAmount); const creditDelta = creditAfter - creditBefore; expectedNodeBalanceChange += creditDelta; const data2 = await getData(); if (!data1.deployed) { assert.equal(data2.deployed, true, 'Megapool was not deployed'); } // Confirm state changes to node const numValidatorsDelta = data2.numValidators - data1.numValidators; const numGlobalValidatorsDelta = data2.numGlobalValidators - data1.numGlobalValidators; const numExpressTicketsDelta = data2.numExpressTickets - data1.numExpressTickets; const assignedValueDelta = data2.assignedValue - data1.assignedValue; const nodeBondDelta = data2.nodeBond - data1.nodeBond; const nodeQueuedBondDelta = data2.nodeQueuedBond - data1.nodeQueuedBond; const userCapitalDelta = data2.userCapital - data1.userCapital; const userQueuedCapitalDelta = data2.userQueuedCapital - data1.userQueuedCapital; const expressQueueLengthDelta = data2.expressQueueLength - data1.expressQueueLength; const standardQueueLengthDelta = data2.standardQueueLength - data1.standardQueueLength; const nodeBalanceDelta = data2.nodeBalance - data1.nodeBalance; const nodeEthBondedDelta = data2.nodeEthBonded - data1.nodeEthBonded; const nodeEthBorrowedDelta = data2.nodeEthBorrowed - data1.nodeEthBorrowed; assertBN.equal(nodeEthBondedDelta, totalBond); assertBN.equal(nodeEthBorrowedDelta, ('32'.ether * BigInt(deposits.length)) - totalBond); assertBN.equal(data2.nodeBond + data2.nodeQueuedBond, data2.nodeMegapoolEthBonded); assertBN.equal(data2.userCapital + data2.userQueuedCapital, data2.nodeMegapoolEthBorrowed); assertBN.equal(numValidatorsDelta, BigInt(deposits.length), 'Number of validators did not increase by 1'); assertBN.equal(numGlobalValidatorsDelta, BigInt(deposits.length), 'Number of global validators did not increase by 1'); assertBN.equal(numExpressTicketsDelta, BigInt(-totalExpressTickets), 'Did not consume express tickets'); // Confirm state of new validator const megapool = await getMegapoolForNode(node); assertBN.equal(nodeBondDelta + nodeQueuedBondDelta, totalBond, 'Incorrect node capital'); assertBN.equal(userCapitalDelta + userQueuedCapitalDelta, ('32'.ether * BigInt(deposits.length)) - totalBond, 'Incorrect user capital'); for (let i = 0; i < deposits.length; ++i) { const validatorId = Number(data1.numValidators) + i; const validatorInfo = await getValidatorInfo(megapool, validatorId); assertBN.equal(validatorInfo.lastRequestedValue, '32'.ether / milliToWei, 'Incorrect validator lastRequestedValue'); assertBN.equal(validatorInfo.lastRequestedBond, deposits[i].bondAmount / milliToWei, 'Incorrect validator lastRequestedBond'); assert.equal(validatorInfo.staked, false, 'Incorrect validator status'); assert.equal(validatorInfo.dissolved, false, 'Incorrect validator status'); assert.equal(validatorInfo.exited, false, 'Incorrect validator status'); assert.equal(validatorInfo.expressUsed, deposits[i].useExpressTicket, 'Incorrect validator express ticket usage'); assert.equal(validatorInfo.pubkey, '0x' + pubkeys[i].toString('hex'), 'Incorrect validator pubkey'); } } export async function getMegapoolWithdrawalCredentials(nodeAddress) { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); const megapoolAddress = await rocketMegapoolFactory.getExpectedAddress(nodeAddress); return '0x010000000000000000000000' + megapoolAddress.substr(2); } export async function getMegapoolForNode(node) { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); const megapoolAddress = await rocketMegapoolFactory.getExpectedAddress(node.address); const delegateAbi = artifacts.require('RocketMegapoolDelegate').abi; const proxyAbi = artifacts.require('RocketMegapoolProxy').abi; const combinedAbi = [...delegateAbi, ...proxyAbi].filter(fragment => fragment.type !== 'constructor'); return new ethers.Contract(megapoolAddress, combinedAbi, node); } export async function getMegapoolForNodeAddress(nodeAddress) { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); const megapoolAddress = await rocketMegapoolFactory.getExpectedAddress(nodeAddress); if (!await rocketMegapoolFactory.getMegapoolDeployed(nodeAddress)) { return null; } const delegateAbi = artifacts.require('RocketMegapoolDelegate').abi; const proxyAbi = artifacts.require('RocketMegapoolProxy').abi; const combinedAbi = [...delegateAbi, ...proxyAbi].filter(fragment => fragment.type !== 'constructor'); const [signer] = await ethers.getSigners(); return new ethers.Contract(megapoolAddress, combinedAbi, signer); } export async function findInQueue(megapoolAddress, validatorId, queueKey, indexOffset = 0n, positionOffset = 0n) { const maxSliceLength = 100n; // Number of entries to scan per call validatorId = BigInt(validatorId); const linkedListStorage = await LinkedListStorage.deployed(); const scan = await linkedListStorage.scan(ethers.solidityPackedKeccak256(['string'], [queueKey]), indexOffset, maxSliceLength); for (const entry of scan[0]) { if (entry[0].toLowerCase() === megapoolAddress.toLowerCase()) { if (entry[1] === validatorId) { // Found the entry return positionOffset; } } positionOffset += 1n; } if (scan[1] === 0n) { // We hit the end of the queue without finding the entry return null; } else { // Nothing in this slice, recurse until end of queue is reached return await findInQueue(megapoolAddress, validatorId, queueKey, scan[1], positionOffset); } } export async function calculatePositionInQueue(megapool, validatorId) { const { expressUsed } = await getValidatorInfo(megapool, validatorId); const queueKeyString = expressUsed ? 'deposit.queue.express' : 'deposit.queue.standard'; const position = await findInQueue(megapool.target, validatorId, queueKeyString); if (position === null) { // Not found in the queue return null; } const linkedListStorage = await LinkedListStorage.deployed(); const rocketDepositPool = await RocketDepositPool.deployed(); const rocketDAOProtocolSettingsDeposit = await RocketDAOProtocolSettingsDeposit.deployed(); const expressQueueLength = await linkedListStorage.getLength(ethers.solidityPackedKeccak256(['string'], ['deposit.queue.express'])); const standardQueueLength = await linkedListStorage.getLength(ethers.solidityPackedKeccak256(['string'], ['deposit.queue.standard'])); const queueIndex = await rocketDepositPool.getQueueIndex(); const expressQueueRate = await rocketDAOProtocolSettingsDeposit.getExpressQueueRate(); const queueInterval = expressQueueRate + 1n; if (expressUsed) { let standardEntriesBefore = (position + (queueIndex % queueInterval)) / expressQueueRate; if (standardEntriesBefore > standardQueueLength) { standardEntriesBefore = standardQueueLength; } return position + standardEntriesBefore; } else { let expressEntriesBefore = (position * expressQueueLength) + (expressQueueRate - (queueIndex % queueInterval)); if (expressEntriesBefore > expressQueueLength) { expressEntriesBefore = expressQueueLength; } return position + expressEntriesBefore; } } ================================================ FILE: test/_helpers/minipool.js ================================================ import { RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNode, RocketMinipoolDelegate, RocketMinipoolFactory, RocketMinipoolManager, RocketNetworkPrices, RocketNodeDeposit, RocketNodeStaking, } from '../_utils/artifacts'; import { getDepositDataRoot, getValidatorPubkey, getValidatorSignature } from '../_utils/beacon'; import { assertBN } from './bn'; import * as assert from 'assert'; // Possible states that a proposal may be in export const minipoolStates = { Initialised: 0, Prelaunch: 1, Staking: 2, Withdrawable: 3, Dissolved: 4, }; // Get the number of minipools a node has export async function getNodeMinipoolCount(nodeAddress) { const rocketMinipoolManager = await RocketMinipoolManager.deployed(); return rocketMinipoolManager.getNodeMinipoolCount(nodeAddress); } // Get the number of minipools a node has in Staking status export async function getNodeStakingMinipoolCount(nodeAddress) { const rocketMinipoolManager = await RocketMinipoolManager.deployed(); return rocketMinipoolManager.getNodeStakingMinipoolCount(nodeAddress); } // Get the number of minipools a node has in that are active export async function getNodeActiveMinipoolCount(nodeAddress) { const rocketMinipoolManager = await RocketMinipoolManager.deployed(); return rocketMinipoolManager.getNodeActiveMinipoolCount(nodeAddress); } // Get the minimum required RPL stake for a minipool export async function getMinipoolMinimumRPLStake() { // Load contracts const [ rocketDAOProtocolSettingsMinipool, rocketNetworkPrices, rocketDAOProtocolSettingsNode, ] = await Promise.all([ RocketDAOProtocolSettingsMinipool.deployed(), RocketNetworkPrices.deployed(), RocketDAOProtocolSettingsNode.deployed(), ]); // Load data let [depositUserAmount, minMinipoolStake, rplPrice] = await Promise.all([ rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount(), rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake(), rocketNetworkPrices.getRPLPrice(), ]); // Calculate & return return depositUserAmount * minMinipoolStake / rplPrice; } // Get the minimum required RPL stake for a minipool export async function getMinipoolMaximumRPLStake() { // Load contracts const [ rocketDAOProtocolSettingsMinipool, rocketNetworkPrices, rocketDAOProtocolSettingsNode, ] = await Promise.all([ RocketDAOProtocolSettingsMinipool.deployed(), RocketNetworkPrices.deployed(), RocketDAOProtocolSettingsNode.deployed(), ]); // Load data let [depositUserAmount, maxMinipoolStake, rplPrice] = await Promise.all([ rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount(), rocketDAOProtocolSettingsNode.getMaximumPerMinipoolStake(), rocketNetworkPrices.getRPLPrice(), ]); // Calculate & return return depositUserAmount * maxMinipoolStake / rplPrice; } let minipoolSalt = 1; // Create a minipool export async function createMinipool(txOptions, salt = null) { return createMinipoolWithBondAmount(txOptions.value, txOptions, salt); } export async function createMinipoolWithBondAmount(bondAmount, txOptions, salt = null) { // Load contracts const [ rocketMinipoolFactory, rocketNodeDeposit, rocketNodeStaking, ] = await Promise.all([ RocketMinipoolFactory.deployed(), RocketNodeDeposit.deployed(), RocketNodeStaking.deployed(), ]); // Get minipool contract bytecode let contractBytecode; if (salt === null) { salt = minipoolSalt++; } let minipoolAddress = (await rocketMinipoolFactory.getExpectedAddress(txOptions.from, salt)).substr(2); let withdrawalCredentials = '0x010000000000000000000000' + minipoolAddress; // Make node deposit const ethBorrowed1 = await rocketNodeStaking.getNodeETHBorrowed(txOptions.from); // Get validator deposit data let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); if (txOptions.value === bondAmount) { await rocketNodeDeposit.connect(txOptions.from).deposit(bondAmount, '0'.ether, depositData.pubkey, depositData.signature, depositDataRoot, salt, '0x' + minipoolAddress, txOptions); } else { await rocketNodeDeposit.connect(txOptions.from).depositWithCredit(bondAmount, '0'.ether, depositData.pubkey, depositData.signature, depositDataRoot, salt, '0x' + minipoolAddress, txOptions); } const ethBorrowed2 = await rocketNodeStaking.getNodeETHBorrowed(txOptions.from); // Expect node's ETH borrowed to be increased by (32 - bondAmount) assertBN.equal(ethBorrowed2 - ethBorrowed1, '32'.ether - bondAmount, 'Incorrect ETH borrowed'); return RocketMinipoolDelegate.at('0x' + minipoolAddress); } // Create a vacant minipool export async function createVacantMinipool(bondAmount, txOptions, salt = null, currentBalance = '32'.ether, pubkey = null) { // Load contracts const [ rocketMinipoolFactory, rocketNodeDeposit, rocketNodeStaking, ] = await Promise.all([ RocketMinipoolFactory.deployed(), RocketNodeDeposit.deployed(), RocketNodeStaking.deployed(), ]); if (salt === null) { salt = minipoolSalt++; } if (pubkey === null) { pubkey = getValidatorPubkey(); } const minipoolAddress = (await rocketMinipoolFactory.getExpectedAddress(txOptions.from, salt)).substr(2); const ethBorrowed1 = await rocketNodeStaking.getNodeETHBorrowed(txOptions.from); await rocketNodeDeposit.connect(txOptions.from).createVacantMinipool(bondAmount, '0'.ether, pubkey, salt, '0x' + minipoolAddress, currentBalance, txOptions); const ethBorrowed2 = await rocketNodeStaking.getNodeETHBorrowed(txOptions.from); // Expect node's ETH borrowed to be increased by (32 - bondAmount) assertBN.equal(ethBorrowed2 - ethBorrowed1, '32'.ether - bondAmount, 'Incorrect ETH borrowed'); return RocketMinipoolDelegate.at('0x' + minipoolAddress); } // Refund node ETH from a minipool export async function refundMinipoolNodeETH(minipool, txOptions) { await minipool.connect(txOptions.from).refund(txOptions); } // Progress a minipool to staking export async function stakeMinipool(minipool, txOptions) { // Get contracts const rocketMinipoolManager = await RocketMinipoolManager.deployed(); // Get minipool validator pubkey const validatorPubkey = await rocketMinipoolManager.getMinipoolPubkey(minipool.target); // Get minipool withdrawal credentials let withdrawalCredentials = await rocketMinipoolManager.getMinipoolWithdrawalCredentials(minipool.target); // Check if legacy or new minipool let legacy = Number(await minipool.getDepositType()) !== 4; // Get validator deposit data let depositData; if (legacy) { depositData = { pubkey: Buffer.from(validatorPubkey.substr(2), 'hex'), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(16000000000), // gwei signature: getValidatorSignature(), }; } else { depositData = { pubkey: Buffer.from(validatorPubkey.substr(2), 'hex'), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(31000000000), // gwei signature: getValidatorSignature(), }; } let depositDataRoot = getDepositDataRoot(depositData); // Stake await minipool.connect(txOptions.from).stake(depositData.signature, depositDataRoot, txOptions); } // Promote a minipool to staking export async function promoteMinipool(minipool, txOptions) { await minipool.connect(txOptions.from).promote(txOptions); // Expect pubkey -> minipool mapping still exists const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const actualPubKey = await rocketMinipoolManager.getMinipoolPubkey(minipool.target); const reverseAddress = await rocketMinipoolManager.getMinipoolByPubkey(actualPubKey); assert.equal(reverseAddress, minipool.target); } // Dissolve a minipool export async function dissolveMinipool(minipool, txOptions) { await minipool.connect(txOptions.from).dissolve(txOptions); } // Close a dissolved minipool and destroy it export async function closeMinipool(minipool, txOptions) { await minipool.connect(txOptions.from).close(txOptions); } ================================================ FILE: test/_helpers/network.js ================================================ import { RocketNetworkBalances, RocketNetworkFees, RocketNetworkPrices, RocketNetworkVoting, } from '../_utils/artifacts'; // Get the network total ETH balance export async function getTotalETHBalance() { const rocketNetworkBalances = await RocketNetworkBalances.deployed(); return rocketNetworkBalances.getTotalETHBalance(); } // Get the network staking ETH balance export async function getStakingETHBalance() { const rocketNetworkBalances = await RocketNetworkBalances.deployed(); return rocketNetworkBalances.getStakingETHBalance(); } // Get the network ETH utilization rate export async function getETHUtilizationRate() { const rocketNetworkBalances = await RocketNetworkBalances.deployed(); return rocketNetworkBalances.getETHUtilizationRate(); } // Submit network balances export async function submitBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions) { const rocketNetworkBalances = await RocketNetworkBalances.deployed(); await rocketNetworkBalances.connect(txOptions.from).submitBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions); } // Submit network token prices export async function submitPrices(block, slotTimestamp, rplPrice, txOptions) { const rocketNetworkPrices = await RocketNetworkPrices.deployed(); await rocketNetworkPrices.connect(txOptions.from).submitPrices(block, slotTimestamp, rplPrice, txOptions); } // Get network RPL price export async function getRPLPrice() { const rocketNetworkPrices = await RocketNetworkPrices.deployed(); return rocketNetworkPrices.getRPLPrice(); } // Get the network node demand export async function getNodeDemand() { const rocketNetworkFees = await RocketNetworkFees.deployed(); return rocketNetworkFees.getNodeDemand(); } // Get the current network node fee export async function getNodeFee() { const rocketNetworkFees = await RocketNetworkFees.deployed(); return rocketNetworkFees.getNodeFee(); } // Get the network node fee for a node demand value export async function getNodeFeeByDemand(nodeDemand) { const rocketNetworkFees = await RocketNetworkFees.deployed(); return rocketNetworkFees.getNodeFeeByDemand(nodeDemand); } export async function setDelegate(nodeAddress, txOptions) { const rocketNetworkVoting = await RocketNetworkVoting.deployed(); await rocketNetworkVoting.connect(txOptions.from).setDelegate(nodeAddress, txOptions); } ================================================ FILE: test/_helpers/node.js ================================================ import { RocketDAONodeTrusted, RocketDAONodeTrustedActions, RocketDAONodeTrustedSettingsMembers, RocketMinipoolFactory, RocketNetworkVoting, RocketNodeDeposit, RocketNodeManager, RocketNodeStaking, RocketStorage, RocketTokenRPL, } from '../_utils/artifacts'; import { setDaoNodeTrustedBootstrapMember } from '../dao/scenario-dao-node-trusted-bootstrap'; import { daoNodeTrustedMemberJoin } from '../dao/scenario-dao-node-trusted'; import { mintDummyRPL } from '../token/scenario-rpl-mint-fixed'; import { burnFixedRPL } from '../token/scenario-rpl-burn-fixed'; import { allowDummyRPL } from '../token/scenario-rpl-allow-fixed'; import { getDepositDataRoot, getValidatorPubkey, getValidatorSignature } from '../_utils/beacon'; import { assertBN } from './bn'; import * as assert from 'assert'; // Get a node's RPL stake export async function getNodeStakedRPL(nodeAddress) { const rocketNodeStaking = await RocketNodeStaking.deployed(); return rocketNodeStaking.getNodeStakedRPL(nodeAddress); } // Get a node's effective RPL stake export async function getNodeEffectiveRPLStake(nodeAddress) { const rocketNodeStaking = await RocketNodeStaking.deployed(); return rocketNodeStaking.getNodeEffectiveRPLStake(nodeAddress); } // Get a node's minipool RPL stake export async function getNodeMinimumRPLStake(nodeAddress) { const rocketNodeStaking = await RocketNodeStaking.deployed(); return rocketNodeStaking.getNodeMinimumRPLStake(nodeAddress); } // Register a node export async function registerNode(txOptions) { const rocketNodeManager = (await RocketNodeManager.deployed()); await rocketNodeManager.connect(txOptions.from).registerNode('Australia/Brisbane'); } // Get number of nodes export async function getNodeCount() { const rocketNodeManager = await RocketNodeManager.deployed(); return rocketNodeManager.getNodeCount(); } // Make a node a trusted dao member, only works in bootstrap mode (< 3 trusted dao members) export async function setNodeTrusted(_account, _id, _url, owner) { // Mints fixed supply RPL, burns that for new RPL and gives it to the account let rplMint = async function(_account, _amount) { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // Mint RPL fixed supply for the users to simulate current users having RPL await mintDummyRPL(_account, _amount, { from: owner }); // Mint a large amount of dummy RPL to owner, who then burns it for real RPL which is sent to nodes for testing below await allowDummyRPL(rocketTokenRPL.target, _amount, { from: _account }); // Burn existing fixed supply RPL for new RPL await burnFixedRPL(_amount, { from: _account }); }; // Allow the given account to spend this users RPL let rplAllowanceDAO = async function(_account, _amount) { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); // Approve now await rocketTokenRPL.connect(_account).approve(rocketDAONodeTrustedActions.target, _amount, { from: _account }); }; // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How much RPL is required for a trusted node bond? let rplBondAmount = await daoNodesettings.getRPLBond(); // Mint RPL bond required for them to join await rplMint(_account, rplBondAmount); // Set allowance for the Vault to grab the bond await rplAllowanceDAO(_account, rplBondAmount); // Create invites for them to become a member await setDaoNodeTrustedBootstrapMember(_id, _url, _account, { from: owner }); // Now get them to join await daoNodeTrustedMemberJoin({ from: _account }); // Check registration was successful and details are correct const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const id = await rocketDAONodeTrusted.getMemberID(_account); assert.equal(id, _id, 'Member ID is wrong'); const url = await rocketDAONodeTrusted.getMemberUrl(_account); assert.equal(url, _url, 'Member URL is wrong'); const joinedTime = await rocketDAONodeTrusted.getMemberJoinedTime(_account); assert.notEqual(joinedTime, 0n, 'Member joined time is wrong'); const valid = await rocketDAONodeTrusted.getMemberIsValid(_account); assert.equal(valid, true, 'Member valid flag is not set'); } // Set a withdrawal address for a node export async function setNodeWithdrawalAddress(nodeAddress, withdrawalAddress, txOptions) { const rocketStorage = await RocketStorage.deployed(); await rocketStorage.connect(txOptions.from).setWithdrawalAddress(nodeAddress, withdrawalAddress, true, txOptions); } // Set an RPL withdrawal address for a node export async function setNodeRPLWithdrawalAddress(nodeAddress, rplWithdrawalAddress, txOptions) { const rocketNodeManager = await RocketNodeManager.deployed(); await rocketNodeManager.connect(txOptions.from).setRPLWithdrawalAddress(nodeAddress, rplWithdrawalAddress, true, txOptions); } // Submit a node RPL stake export async function nodeStakeRPL(amount, txOptions) { const [rocketNodeStaking, rocketTokenRPL] = await Promise.all([ RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), ]); await rocketTokenRPL.connect(txOptions.from).approve(rocketNodeStaking.target, amount); const before = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); await rocketNodeStaking.connect(txOptions.from).stakeRPL(amount); const after = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); assertBN.equal(after - before, amount, 'Staking balance did not increase by amount staked'); } // Delegate voting power export async function nodeSetDelegate(to, txOptions) { const rocketNetworkVoting = (await RocketNetworkVoting.deployed()).connect(txOptions.from); await rocketNetworkVoting.setDelegate(to, txOptions); const newDelegate = await rocketNetworkVoting.getCurrentDelegate(txOptions.from); assert.equal(newDelegate, to); } // Submit a node RPL stake on behalf of another node export async function nodeStakeRPLFor(nodeAddress, amount, txOptions) { const [rocketNodeStaking, rocketTokenRPL] = await Promise.all([ RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), ]); await rocketTokenRPL.connect(txOptions.from).approve(rocketNodeStaking.target, amount, txOptions); const before = await rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress); await rocketNodeStaking.connect(txOptions.from).stakeRPLFor(nodeAddress, amount, txOptions); const after = await rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress); assertBN.equal(after - before, amount, 'Staking balance did not increase by amount staked'); } // Deposits ETH into a node operator's balance export async function nodeDepositEthFor(nodeAddress, txOptions) { const [rocketNodeDeposit] = await Promise.all([ RocketNodeDeposit.deployed(), ]); const before = await rocketNodeDeposit.getNodeEthBalance(nodeAddress); await rocketNodeDeposit.connect(txOptions.from).depositEthFor(nodeAddress, txOptions); const after = await rocketNodeDeposit.getNodeEthBalance(nodeAddress); assertBN.equal(after - before, txOptions.value, 'ETH balance did not increase by msg.value'); } // Sets allow state for staking on behalf export async function setStakeRPLForAllowed(caller, state, txOptions) { const [rocketNodeStaking] = await Promise.all([ RocketNodeStaking.deployed(), ]); await rocketNodeStaking.connect(txOptions.from)['setStakeRPLForAllowed(address,bool)'](caller, state, txOptions); } // Sets allow state for staking on behalf export async function setStakeRPLForAllowedWithNodeAddress(nodeAddress, caller, state, txOptions) { const rocketNodeStaking = (await RocketNodeStaking.deployed()).connect(txOptions.from); await rocketNodeStaking['setStakeRPLForAllowed(address,address,bool)'](nodeAddress, caller, state, txOptions); } // Withdraw a node RPL stake export async function nodeWithdrawRPL(amount, txOptions) { const rocketNodeStaking = (await RocketNodeStaking.deployed()).connect(txOptions.from); await rocketNodeStaking['withdrawRPL(uint256)'](amount, txOptions); } // Set allow state for RPL locking export async function setRPLLockingAllowed(node, state, txOptions) { const rocketNodeStaking = (await RocketNodeStaking.deployed()).connect(txOptions.from); await rocketNodeStaking.connect(txOptions.from).setRPLLockingAllowed(node, state); } // Make a node deposit let minipoolSalt = 0; export async function nodeDeposit(txOptions) { // Load contracts const [ rocketMinipoolFactory, rocketNodeDeposit, rocketStorage, ] = await Promise.all([ RocketMinipoolFactory.deployed(), RocketNodeDeposit.deployed(), RocketStorage.deployed(), ]); const salt = minipoolSalt++; const minipoolAddress = (await rocketMinipoolFactory.getExpectedAddress(txOptions.from.address, salt)).substr(2); let withdrawalCredentials = '0x010000000000000000000000' + minipoolAddress; // Get validator deposit data let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // 1 ETH in gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); // Make node deposit await rocketNodeDeposit.connect(txOptions.from).deposit(txOptions.value, '0'.ether, depositData.pubkey, depositData.signature, depositDataRoot, salt, '0x' + minipoolAddress, txOptions); } // Get a node's deposit credit balance export async function getNodeDepositCredit(nodeAddress) { const rocketNodeDeposit = (await RocketNodeDeposit.deployed()); return rocketNodeDeposit.getNodeDepositCredit(nodeAddress); } // Get a node's effective RPL stake export async function getNodeAverageFee(nodeAddress) { const rocketNodeManager = (await RocketNodeManager.deployed()); return rocketNodeManager.getAverageNodeFee(nodeAddress); } ================================================ FILE: test/_helpers/settings.js ================================================ import { RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsNode, } from '../_utils/artifacts'; // Auction settings export async function getAuctionSetting(setting) { const rocketAuctionSettings = await RocketDAOProtocolSettingsAuction.deployed(); return rocketAuctionSettings['get' + setting](); } // Deposit settings export async function getDepositSetting(setting) { const rocketDAOProtocolSettingsDeposit = await RocketDAOProtocolSettingsDeposit.deployed(); return rocketDAOProtocolSettingsDeposit['get' + setting](); } // Minipool settings export async function getMinipoolSetting(setting) { const rocketDAOProtocolSettingsMinipool = await RocketDAOProtocolSettingsMinipool.deployed(); return rocketDAOProtocolSettingsMinipool['get' + setting](); } // Network settings export async function getNetworkSetting(setting) { const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); return rocketDAOProtocolSettingsNetwork['get' + setting](); } // Node settings export async function getNodeSetting(setting) { const rocketDAOProtocolSettingsNode = await RocketDAOProtocolSettingsNode.deployed(); return rocketDAOProtocolSettingsNode['get' + setting](); } ================================================ FILE: test/_helpers/tokens.js ================================================ import { RocketTokenDummyRPL, RocketTokenRETH, RocketTokenRPL } from '../_utils/artifacts'; // Get the RPL balance of an address export async function getRplBalance(address) { const rocketTokenRPL = await RocketTokenRPL.deployed(); return rocketTokenRPL.balanceOf(address); } // Get the rETH balance of an address export async function getRethBalance(address) { const rocketTokenRETH = await RocketTokenRETH.deployed(); return rocketTokenRETH.balanceOf(address); } // Get the current rETH exchange rate export async function getRethExchangeRate() { const rocketTokenRETH = await RocketTokenRETH.deployed(); return rocketTokenRETH.getExchangeRate(); } // Get the current rETH collateral rate export async function getRethCollateralRate() { const rocketTokenRETH = await RocketTokenRETH.deployed(); return rocketTokenRETH.getCollateralRate(); } // Get the current rETH token supply export async function getRethTotalSupply() { const rocketTokenRETH = await RocketTokenRETH.deployed(); return rocketTokenRETH.totalSupply(); } // Mint RPL to an address export async function mintRPL(owner, to, amount) { // Load contracts const [rocketTokenDummyRPL, rocketTokenRPL] = await Promise.all([ RocketTokenDummyRPL.deployed(), RocketTokenRPL.deployed(), ]); // Mint dummy RPL to address await rocketTokenDummyRPL.connect(owner).mint(to, amount); // Swap dummy RPL for RPL await rocketTokenDummyRPL.connect(to).approve(rocketTokenRPL.target, amount); await rocketTokenRPL.connect(to).swapTokens(amount); } // Approve RPL to be spend by an address export async function approveRPL(spender, amount, txOptions) { const rocketTokenRPL = await RocketTokenRPL.deployed(); await rocketTokenRPL.connect(txOptions.from).approve(spender, amount, txOptions); } export async function depositExcessCollateral(txOptions) { const rocketTokenRETH = await RocketTokenRETH.deployed(); await rocketTokenRETH.connect(txOptions.from).depositExcessCollateral(txOptions); } ================================================ FILE: test/_helpers/verify.js ================================================ import path from 'path'; import axios from 'axios'; import * as querystring from 'node:querystring'; import fs from 'fs'; function treeShake(sources, file) { const returns = {}; if (!(file in sources)) { throw new Error(`Cannot find source for file ${file}`); } const content = sources[file].content; const importsOld = content.matchAll(/import "(.+)"/g); const importsNew = content.matchAll(/import {.+} from "(.+)"/g); const imports = [...importsOld, ...importsNew]; for (const match of imports) { let i = match[1]; if (!i.startsWith('@')) { const dir = path.dirname(file); i = path.normalize(path.join(dir, i)); } const subImports = treeShake(sources, i); for (const si in subImports) { returns[si] = subImports[si]; } } returns[file] = sources[file]; return returns; } const defaultOpts = { license: 5, apiKey: null, chain: 'mainnet', preamble: '', }; const endpoint = 'https://api.etherscan.io/v2/api' const chainIdMap = { 'mainnet': '1', 'hoodi': '560048', }; export class EtherscanVerifier { constructor(buildInfos, opts = {}) { this.buildInfos = buildInfos; this.opts = { ...defaultOpts, ...opts }; } log(string = '\n', color = 'gray') { let colorCodes = { 'white': 0, 'gray': 37, 'red': 31, 'blue': 34, 'green': 32, }; console.log('\x1b[%sm%s\x1b[0m', colorCodes[color], string); } async verifyAll(contracts) { const results = {}; this.log('# Verifying contracts', 'blue') for (const contract of contracts) { results[contract.contractName] = await this.verify(contract.buildInfoId, contract.sourceName, contract.contractName, contract.address, contract.constructorArgs); // Avoid Etherscans 3/s rate limit await new Promise(resolve => setTimeout(resolve, 500)); } return results; } async verify(buildInfoId, sourceName, contractName, address, constructorArgs) { this.log(` - Attempting to verify ${contractName} @ ${address}`, 'white'); // Slice of 0x if supplied if (constructorArgs.startsWith('0x')) { constructorArgs = constructorArgs.substr(2); } const buildInfo = this.buildInfos[buildInfoId]; if (buildInfo === undefined) { this.log(` - Failed to find relevant build info`, 'red'); return null; } let sources; try { sources = treeShake(buildInfo.input.sources, sourceName); } catch (error) { this.log(` - Failed to shake source tree`, 'red'); console.error(error); return false; } sources = this.applyPreamble(sources); const inputJSON = { sources, language: buildInfo.input.language, settings: buildInfo.input.settings, }; return await this.submitVerification(inputJSON, sourceName + ":" + contractName, address, buildInfo.solcLongVersion, constructorArgs); } getStandardJsonInput(buildInfoId, contractName, sourceName) { const buildInfo = this.buildInfos[buildInfoId]; if (buildInfo === undefined) { return null; } let sources = treeShake(buildInfo.input.sources, sourceName); sources = this.applyPreamble(sources); return { sources, language: buildInfo.input.language, settings: buildInfo.input.settings, }; } applyPreamble(sources) { let prefixedSources = {}; for (let contractPath in sources) { // If the path begins with project: then it's one of our files, so add the preamble if (contractPath.startsWith('contracts/')) { contractPath = contractPath.substring(''.length); const content = this.opts.preamble + sources[contractPath].content; prefixedSources[contractPath] = { content }; } else { prefixedSources[contractPath] = { content: sources[contractPath].content }; } } return prefixedSources; } async getVerificationStatus(guid) { const result = await axios.get(`${endpoint}?chainid=${chainIdMap[this.opts.chain]}&module=contract&action=checkverifystatus&apikey=${this.opts.apiKey}&guid=${guid}`); return result.data; } async isVerified(address) { const result = await axios.get(`${endpoint}?chainid=${chainIdMap[this.opts.chain]}&module=contract&action=getabi&apikey=${this.opts.apiKey}&address=${address}`); return result.data.status !== '0'; } async submitVerification(inputJSON, contractName, address, compilerVersion, constructorArgs) { // Check if it's already verified if (await this.isVerified(address)) { this.log(` - Already verified`, 'green'); return; } this.log(` - Submitting to Etherscan`); const payload = { apikey: this.opts.apiKey, module: 'contract', action: 'verifysourcecode', contractaddress: address, sourceCode: JSON.stringify(inputJSON), codeformat: 'solidity-standard-json-input', contractname: contractName, compilerversion: 'v' + compilerVersion, optimizationUsed: 0, // unused runs: 200, // unused evmversion: '', // unused constructorArguements: constructorArgs, licenseType: this.opts.license, libraryname1: '', libraryaddress1: '', libraryname2: '', libraryaddress2: '', libraryname3: '', libraryaddress3: '', libraryname4: '', libraryaddress4: '', libraryname5: '', libraryaddress5: '', libraryname6: '', libraryaddress6: '', libraryname7: '', libraryaddress7: '', libraryname8: '', libraryaddress8: '', libraryname9: '', libraryaddress9: '', libraryname10: '', libraryaddress10: '', }; // Submit to API const formData = querystring.stringify(payload); const result = await axios.post(`${endpoint}?chainid=${chainIdMap[this.opts.chain]}`, formData); // Check result if (result.data.status !== '1') { this.log(` - Failed to submit`, 'red'); console.error(result.data); return null; } else { this.log(` - GUID: ${result.data.result}`); return result.data.result; } } } ================================================ FILE: test/_utils/artifacts.js ================================================ import { decompressABI } from './contract'; const hre = require('hardhat'); const ethers = hre.ethers; class Artifact { constructor(name) { this.name = name; const hreArtifact = hre.artifacts.readArtifactSync(name); this.abi = hreArtifact.abi; this.contractName = hreArtifact.contractName; this.sourceName = hreArtifact.sourceName; this.instance = null; } async deployed() { return this.instance; } setAsDeployed(instance) { this.instance = instance; } async newImmediate(...args) { this.instance = await (await ethers.getContractFactory(this.name)).deploy(...args); return this.instance; } async new(...args) { this.instance = await (await ethers.getContractFactory(this.name)).deploy(...args); await this.instance.waitForDeployment(); return this.instance; } async clone(...args) { const instance = await (await ethers.getContractFactory(this.name)).deploy(...args); await instance.waitForDeployment(); return instance; } at(address) { return new ethers.Contract(address, this.abi, hre.ethers.provider); } async fromDeployment(rocketStorage, contractName = null) { if (contractName === null) { contractName = this.name.charAt(0).toLowerCase() + this.name.slice(1); } const addressKey = ethers.solidityPackedKeccak256(['string', 'string'], ['contract.address', contractName]); const abiKey = ethers.solidityPackedKeccak256(['string', 'string'], ['contract.abi', contractName]); const address = await rocketStorage['getAddress(bytes32)'](addressKey); const abiRaw = await rocketStorage['getString(bytes32)'](abiKey); if (address === '0x0000000000000000000000000000000000000000' || abiRaw === '') { return; } this.abi = decompressABI(abiRaw); this.instance = new ethers.Contract(address, this.abi, hre.ethers.provider); } } export class Artifacts { constructor() { this.artifacts = {}; } require(name) { if (!this.artifacts.hasOwnProperty(name)) { this.artifacts[name] = new Artifact(name); } return this.artifacts[name]; } async loadFromDeployment(rocketStorageAddress) { RocketStorage.instance = this.artifacts['RocketStorage'].at(rocketStorageAddress); // Map between network contract name and actual contract name const mapping = { 'RocketTokenDummyRPL': 'rocketTokenRPLFixedSupply', 'LinkedListStorageHelper': 'linkedListStorage', }; for (const name in this.artifacts) { switch (name) { case 'RocketStorage': break; default: if (mapping.hasOwnProperty(name)) { await this.artifacts[name].fromDeployment(RocketStorage.instance, mapping[name]); } else { await this.artifacts[name].fromDeployment(RocketStorage.instance); } } } } } export const artifacts = new Artifacts(); export const RocketAuctionManager = artifacts.require('RocketAuctionManager'); export const RocketClaimDAO = artifacts.require('RocketClaimDAO'); export const RocketDAONodeTrusted = artifacts.require('RocketDAONodeTrusted'); export const RocketDAONodeTrustedActions = artifacts.require('RocketDAONodeTrustedActions'); export const RocketDAONodeTrustedProposals = artifacts.require('RocketDAONodeTrustedProposals'); export const RocketDAONodeTrustedSettingsMembers = artifacts.require('RocketDAONodeTrustedSettingsMembers'); export const RocketDAONodeTrustedSettingsProposals = artifacts.require('RocketDAONodeTrustedSettingsProposals'); export const RocketDAONodeTrustedSettingsMinipool = artifacts.require('RocketDAONodeTrustedSettingsMinipool'); export const RocketDAONodeTrustedUpgrade = artifacts.require('RocketDAONodeTrustedUpgrade'); export const RocketDAOProtocol = artifacts.require('RocketDAOProtocol'); export const RocketDAOProtocolProposals = artifacts.require('RocketDAOProtocolProposals'); export const RocketDAOProtocolProposal = artifacts.require('RocketDAOProtocolProposal'); export const RocketDAOProtocolSettingsAuction = artifacts.require('RocketDAOProtocolSettingsAuction'); export const RocketDAOProtocolSettingsDeposit = artifacts.require('RocketDAOProtocolSettingsDeposit'); export const RocketDAOProtocolSettingsInflation = artifacts.require('RocketDAOProtocolSettingsInflation'); export const RocketDAOProtocolSettingsNetwork = artifacts.require('RocketDAOProtocolSettingsNetwork'); export const RocketDAOProtocolSettingsNode = artifacts.require('RocketDAOProtocolSettingsNode'); export const RocketDAOProtocolSettingsRewards = artifacts.require('RocketDAOProtocolSettingsRewards'); export const RocketDAOProtocolSettingsProposals = artifacts.require('RocketDAOProtocolSettingsProposals'); export const RocketDAOProtocolSettingsSecurity = artifacts.require('RocketDAOProtocolSettingsSecurity'); export const RocketDAOProtocolVerifier = artifacts.require('RocketDAOProtocolVerifier'); export const RocketDAOProposal = artifacts.require('RocketDAOProposal'); export const RocketDAOSecurityActions = artifacts.require('RocketDAOSecurityActions'); export const RocketDAOSecurityProposals = artifacts.require('RocketDAOSecurityProposals'); export const RocketDAOSecurityUpgrade = artifacts.require('RocketDAOSecurityUpgrade'); export const RocketDAOSecurity = artifacts.require('RocketDAOSecurity'); export const RocketMinipoolPenalty = artifacts.require('RocketMinipoolPenalty'); export const RocketMinipoolManager = artifacts.require('RocketMinipoolManager'); export const RocketNetworkBalances = artifacts.require('RocketNetworkBalances'); export const RocketNetworkPenalties = artifacts.require('RocketNetworkPenalties'); export const RocketNetworkFees = artifacts.require('RocketNetworkFees'); export const RocketNetworkPrices = artifacts.require('RocketNetworkPrices'); export const RocketNodeManager = artifacts.require('RocketNodeManager'); export const RocketNodeStaking = artifacts.require('RocketNodeStaking'); export const RocketNodeDistributorFactory = artifacts.require('RocketNodeDistributorFactory'); export const RocketNodeDistributorDelegate = artifacts.require('RocketNodeDistributorDelegate'); export const RocketRewardsPool = artifacts.require('RocketRewardsPool'); export const RocketMerkleDistributorMainnet = artifacts.require('RocketMerkleDistributorMainnet'); export const RocketSmoothingPool = artifacts.require('RocketSmoothingPool'); export const RocketStorage = artifacts.require('RocketStorage'); export const RocketTokenDummyRPL = artifacts.require('RocketTokenDummyRPL'); export const RocketTokenRETH = artifacts.require('RocketTokenRETH'); export const RocketTokenRPL = artifacts.require('RocketTokenRPL'); export const RocketVault = artifacts.require('RocketVault'); export const RevertOnTransfer = artifacts.require('RevertOnTransfer'); export const PenaltyTest = artifacts.require('PenaltyTest'); export const SnapshotTest = artifacts.require('SnapshotTest'); export const SnapshotTimeTest = artifacts.require('SnapshotTimeTest'); export const RocketMegapoolFactory = artifacts.require('RocketMegapoolFactory'); export const RocketMegapoolDelegate = artifacts.require('RocketMegapoolDelegate'); export const RocketMegapoolProxy = artifacts.require('RocketMegapoolProxy'); export const RocketMegapoolManager = artifacts.require('RocketMegapoolManager'); export const RocketMegapoolPenalties = artifacts.require('RocketMegapoolPenalties'); export const RocketMinipoolFactory = artifacts.require('RocketMinipoolFactory'); export const RocketMinipoolBase = artifacts.require('RocketMinipoolBase'); export const RocketMinipoolQueue = artifacts.require('RocketMinipoolQueue'); export const RocketNodeDeposit = artifacts.require('RocketNodeDeposit'); export const RocketMinipoolDelegate = artifacts.require('RocketMinipoolDelegate'); export const RocketDAOProtocolSettingsMinipool = artifacts.require('RocketDAOProtocolSettingsMinipool'); export const RocketDAOProtocolSettingsMegapool = artifacts.require('RocketDAOProtocolSettingsMegapool'); export const LinkedListStorage = artifacts.require('LinkedListStorageHelper'); export const StorageHelper = artifacts.require('StorageHelper'); export const RocketDepositPool = artifacts.require('RocketDepositPool'); export const RocketMinipoolBondReducer = artifacts.require('RocketMinipoolBondReducer'); export const RocketNetworkSnapshots = artifacts.require('RocketNetworkSnapshots'); export const RocketNetworkSnapshotsTime = artifacts.require('RocketNetworkSnapshotsTime'); export const RocketNetworkVoting = artifacts.require('RocketNetworkVoting'); export const MegapoolUpgradeHelper = artifacts.require('MegapoolUpgradeHelper'); export const StakeHelper = artifacts.require('StakeHelper'); export const BeaconStateVerifier = artifacts.require('BeaconStateVerifierMock'); export const RocketNetworkRevenues = artifacts.require('RocketNetworkRevenues'); ================================================ FILE: test/_utils/beacon.js ================================================ const ssz = require('@chainsafe/ssz'); const types = require('@chainsafe/lodestar-types/lib/ssz/presets/mainnet').types; // Current pubkey index let pubkeyIndex = 0; // Create a new validator pubkey export function getValidatorPubkey() { let index = ++pubkeyIndex; return Buffer.from(index.toString(16).padStart(96, '0'), 'hex'); } // Create a validator signature // TODO: implement correctly once BLS library found export function getValidatorSignature() { return Buffer.from( '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'); } // Create validator deposit data root export function getDepositDataRoot(depositData) { return types.DepositData.hashTreeRoot(depositData); } ================================================ FILE: test/_utils/contract.js ================================================ // Dependencies const pako = require('pako'); // Get arbitrary contract events from a transaction result // txReceipt is the receipt returned from the transaction call // contractAddress is the address of the contract to retrieve events for // eventName is the name of the event to retrieve // eventParams is an array of objects with string 'type' and 'name' keys and an optional boolean 'indexed' key export function getTxContractEvents(txReceipt, contractAddress, eventName, eventParams) { return txReceipt.receipt.rawLogs .filter(log => (log.address.toLowerCase() == contractAddress.toLowerCase())) .filter(log => (log.topics[0] == web3.utils.soliditySha3(eventName + '(' + eventParams.map(param => param.type).join(',') + ')'))) .map(log => web3.eth.abi.decodeLog(eventParams.map(param => { let decodeParam = Object.assign({}, param); if (decodeParam.indexed && (decodeParam.type == 'string' || decodeParam.type == 'bytes')) decodeParam.type = 'bytes32'; // Issues decoding indexed string and bytes parameters return decodeParam; }), log.data, log.topics.slice(1))); } // Compress / decompress contract ABIs export function compressABI(abi) { return Buffer.from(pako.deflate(JSON.stringify(abi))).toString('base64'); } export function decompressABI(abi) { return JSON.parse(pako.inflate(Buffer.from(abi, 'base64'), {to: 'string'})); } ================================================ FILE: test/_utils/evm.js ================================================ // Take a snapshot of the EVM state export function takeSnapshot(web3) { return new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: '2.0', method: 'evm_snapshot', id: (new Date()).getTime(), }, function(err, response) { if (err) { reject(err); } else if (response && response.result) { resolve(response.result); } else { reject('Unknown error'); } }); }); } // Restore a snapshot of EVM state export function revertSnapshot(web3, snapshotId) { return new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: '2.0', method: 'evm_revert', params: [snapshotId], id: (new Date()).getTime(), }, function(err, response) { if (err) { reject(err); } else { resolve(); } }); }); } // Mine a number of blocks export async function mineBlocks(web3, numBlocks) { for (let i = 0; i < numBlocks; ++i) { await new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: '2.0', method: 'evm_mine', id: (new Date()).getTime(), }, function(err, response) { if (err) { reject(err); } else { resolve(); } }); }); } } // Fast-forward time export async function increaseTime(web3, seconds) { await new Promise((resolve, reject) => { web3.currentProvider.send({ jsonrpc: '2.0', method: 'evm_increaseTime', params: [seconds], id: (new Date()).getTime(), }, function(err, response) { if (err) { reject(err); } else { resolve(); } }); }); // Mine a block using the new time await mineBlocks(web3, 1); } // Retrieve current time on block chain export async function getCurrentTime(web3) { return (await web3.eth.getBlock('latest')).timestamp } ================================================ FILE: test/_utils/formatting.js ================================================ // Print pretty test title export function printTitle(user, desc) { return '\x1b[33m' + user + '\u001b[00m: \u001b[01;34m' + desc; } ================================================ FILE: test/_utils/merkle-tree.js ================================================ /** * Modified from https://github.com/Uniswap/merkle-distributor */ const hre = require('hardhat'); const ethers = hre.ethers; function bufferToHex(buffer) { return '0x' + Buffer.from(buffer).toString('hex'); } function keccak256(data) { return Buffer.from(ethers.keccak256(bufferToHex(data)).substr(2), 'hex'); } export class MerkleTree { constructor(elements) { this.elements = [...elements]; // Sort elements this.elements.sort(Buffer.compare); // Deduplicate elements this.elements = MerkleTree.bufDedup(this.elements); // Pad to power of 2 let paddedLen = Math.pow(Math.ceil(Math.log2(this.elements.length)), 2); for (let i = this.elements.length; i < paddedLen; i++) { this.elements.push(Buffer.alloc(32)); } this.bufferElementPositionIndex = this.elements.reduce((memo, el, index) => { memo[bufferToHex(el)] = index; return memo; }, {}); // Create layers this.layers = this.getLayers(this.elements); } getLayers(elements) { if (elements.length === 0) { throw new Error('empty tree'); } const layers = []; layers.push(elements); // Get next layer until we reach the root while (layers[layers.length - 1].length > 1) { layers.push(this.getNextLayer(layers[layers.length - 1])); } return layers; } getNextLayer(elements) { return elements.reduce((layer, el, idx, arr) => { if (idx % 2 === 0) { // Hash the current element with its pair element layer.push(MerkleTree.combinedHash(el, arr[idx + 1])); } return layer; }, []); } static combinedHash(first, second) { return keccak256(MerkleTree.sortAndConcat(first, second)); } getRoot() { return this.layers[this.layers.length - 1][0]; } getHexRoot() { return bufferToHex(this.getRoot()); } getProof(el) { let idx = this.bufferElementPositionIndex[bufferToHex(el)]; if (typeof idx !== 'number') { throw new Error('Element does not exist in Merkle tree'); } return this.layers.reduce((proof, layer) => { if (layer.length > 1) { const pairElement = MerkleTree.getPairElement(idx, layer); // Dangling element is paired with null if (pairElement) { proof.push(pairElement); } else { proof.push(Buffer.alloc(32)); } } idx = Math.floor(idx / 2); return proof; }, []); } getHexProof(el) { const proof = this.getProof(el); return MerkleTree.bufArrToHexArr(proof); } static getPairElement(idx, layer) { const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; if (pairIdx < layer.length) { return layer[pairIdx]; } else { return null; } } static bufDedup(elements) { return elements.filter((el, idx) => { return idx === 0 || !elements[idx - 1].equals(el); }); } static bufArrToHexArr(arr) { if (arr.some((el) => !Buffer.isBuffer(el))) { throw new Error('Array is not an array of buffers'); } return arr.map((el) => '0x' + el.toString('hex')); } static sortAndConcat(...args) { return Buffer.concat([...args].sort(Buffer.compare)); } } export class RewardClaimTree { constructor(balances) { this.tree = new MerkleTree( balances.map(({ address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH }) => { return RewardClaimTree.toNode(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH); }), ); } static verifyProof(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH, proof, root) { let pair = RewardClaimTree.toNode(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH); for (const item of proof) { pair = MerkleTree.combinedHash(pair, item); } return pair.equals(root); } // keccak256(abi.encode(nodeAddress, network, amountRPL, amountSmoothingPoolETH, amountVoterETH)) static toNode(nodeAddress, network, amountRPL, amountSmoothingPoolETH, amountVoterETH) { return Buffer.from( ethers.solidityPackedKeccak256(['address', 'uint256', 'uint256', 'uint256', 'uint256'], [nodeAddress, network, amountRPL, amountSmoothingPoolETH, amountVoterETH]).substr(2), 'hex', ); } getHexRoot() { return this.tree.getHexRoot(); } // returns the hex bytes32 values of the proof getProof(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH) { return this.tree.getHexProof(RewardClaimTree.toNode(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH)); } } // Takes an array of objects with the form [{address, id, network, amountRPL, amountSmoothingPoolETH, amountVoterETH},...] and returns a RewardClaimTree object export function parseRewardsMap(rewards) { // Transform input into a mapping of address => { address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH } const dataByAddress = rewards.reduce((memo, { address, network, trustedNodeRPL, nodeRPL, nodeETH, voterETH }) => { if (!ethers.isAddress(address)) { throw new Error(`Found invalid address: ${address}`); } memo[address] = { address: ethers.getAddress(address), amountRPL: nodeRPL + trustedNodeRPL, amountSmoothingPoolETH: nodeETH, amountVoterETH: voterETH, network: BigInt(network), }; return memo; }, {}); const rewardsPerNetworkBN = rewards.reduce((perNetwork, { network, trustedNodeRPL, nodeRPL, nodeETH, voterETH }) => { if (!(network in perNetwork)) { perNetwork[network] = { RPL: 0n, ETH: 0n, }; } perNetwork[network].RPL = perNetwork[network].RPL + nodeRPL + trustedNodeRPL; perNetwork[network].ETH = perNetwork[network].ETH + nodeETH + voterETH; return perNetwork; }, {}); const rewardsPerNetworkRPL = {}; const rewardsPerNetworkETH = {}; Object.keys(rewardsPerNetworkBN).map(network => rewardsPerNetworkRPL[network] = rewardsPerNetworkBN[network].RPL); Object.keys(rewardsPerNetworkBN).map(network => rewardsPerNetworkETH[network] = rewardsPerNetworkBN[network].ETH); // Sort const sortedAddresses = Object.keys(dataByAddress).sort(); // Construct a tree const tree = new RewardClaimTree( sortedAddresses.map((address) => ({ address: dataByAddress[address].address, network: dataByAddress[address].network, amountRPL: dataByAddress[address].amountRPL, amountSmoothingPoolETH: dataByAddress[address].amountSmoothingPoolETH, amountVoterETH: dataByAddress[address].amountVoterETH, })), ); // Generate claims const claims = sortedAddresses.reduce((memo, _address) => { const { address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH } = dataByAddress[_address]; memo[address] = { network: Number(network), amountRPL: amountRPL, amountSmoothingPoolETH: amountSmoothingPoolETH, amountVoterETH: amountVoterETH, proof: tree.getProof(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH), leaf: RewardClaimTree.toNode(address, network, amountRPL, amountSmoothingPoolETH, amountVoterETH).toString('hex'), }; return memo; }, {}); const totalRewardsRPL = sortedAddresses.reduce( (memo, key) => memo + dataByAddress[key].amountRPL, 0n, ); const totalRewardsETH = sortedAddresses.reduce( (memo, key) => memo + dataByAddress[key].amountSmoothingPoolETH + dataByAddress[key].amountVoterETH, 0n, ); return { tree: tree, proof: { merkleRoot: tree.getHexRoot(), rewardsPerNetworkRPL: rewardsPerNetworkRPL, rewardsPerNetworkETH: rewardsPerNetworkETH, totalRewardsRPL: totalRewardsRPL, totalRewardsETH: totalRewardsETH, claims, }, }; } ================================================ FILE: test/_utils/snapshotting.js ================================================ import currentContext, { after, before, describe as originalDescribe } from 'mocha'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); let globalSnapshot, snapshot; export async function startSnapShot() { snapshot = await helpers.takeSnapshot(); } export async function endSnapShot() { await snapshot.restore(); } export async function globalSnapShot() { if (globalSnapshot) { await globalSnapshot.restore(); } globalSnapshot = await helpers.takeSnapshot(); } const _snapshotDescribe = function(n, tests) { return originalDescribe(n, () => { let describeSnapshot; before(async function() { describeSnapshot = await helpers.takeSnapshot(); }); after(async function() { await describeSnapshot.restore(); }); tests(); }); } _snapshotDescribe.only = function (...args) { return (currentContext.describe || currentContext.suite).only.apply( this, args ); }; _snapshotDescribe.skip = function (...args) { return (currentContext.describe || currentContext.suite).skip.apply( this, args ); }; export const snapshotDescribe = _snapshotDescribe ================================================ FILE: test/_utils/testing.js ================================================ // Assert that a transaction reverts import * as assert from 'assert'; export async function shouldRevert(txPromise, message, expectedErrorMessage = null) { let txSuccess = false; try { await txPromise; txSuccess = true; } catch (e) { // With --via-ir flag, hardhat sometimes can't get the error message if (e.message.indexOf('Transaction reverted and Hardhat couldn\'t infer the reason') !== -1) return; // If we don't need to match a specific error message if (!expectedErrorMessage && e.message.indexOf('VM Exception') === -1) throw e; // If we do if (expectedErrorMessage && e.message.indexOf(expectedErrorMessage) === -1) assert.fail('Expected error message not found, error received: ' + e.message.replace('Returned error:', '')); } finally { if (txSuccess) assert.fail(message); } } // Allows async describe functions export default async function asyncDescribe(desc, run) { const its = {}; return run((testName, func) => { its[testName] = func; }).then(() => { describe(desc, () => { for (const [testName, runFunction] of Object.entries(its)) { it(testName, runFunction); } }); }); } ================================================ FILE: test/_utils/upgrade.js ================================================ import { artifacts } from './artifacts'; ================================================ FILE: test/auction/auction-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { RocketAuctionManager, RocketDAOProtocolSettingsAuction, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { auctionCreateLot, auctionPlaceBid, getLotPriceAtBlock, getLotStartBlock } from '../_helpers/auction'; import { submitPrices } from '../_helpers/network'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { approveRPL, mintRPL } from '../_helpers/tokens'; import { createLot } from './scenario-create-lot'; import { placeBid } from './scenario-place-bid'; import { claimBid } from './scenario-claim-bid'; import { recoverUnclaimedRPL } from './scenario-recover-rpl'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; import { registerNode, setNodeTrusted } from '../_helpers/node'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketAuctionManager', () => { let owner, node, trustedNode, random1, random2; const auctionDuration = 7200; before(async () => { await globalSnapShot(); [ owner, node, trustedNode, random1, random2, ] = await ethers.getSigners(); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.duration', auctionDuration, { from: owner }); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); }); it(printTitle('random address', 'cannot create a lot with an insufficient RPL balance'), async () => { // Attempt to create lot await shouldRevert(createLot({ from: random1, }), 'Created a lot with an insufficient RPL balance'); }); describe('With RPL to auction', () => { const rplAmount = '1600'.ether; before(async () => { // Get contracts const rocketTokenRpl = await RocketTokenRPL.deployed(); const rocketVault = await RocketVault.deployed(); const rocketAuctionManager = await RocketAuctionManager.deployed(); // Mint and send RPL to the auction manager await mintRPL(owner, owner, rplAmount); await approveRPL(rocketVault.target, rplAmount, { from: owner }); await rocketVault.depositToken('rocketAuctionManager', rocketTokenRpl.target, rplAmount, { from: owner }); // Verify RPL balance const rplBalance = await rocketAuctionManager.getTotalRPLBalance(); assertBN.equal(rplAmount, rplBalance, 'Incorrect RPL balance'); }); it(printTitle('random address', 'can create a lot'), async () => { // Create first lot await createLot({ from: random1, }); // Create second lot await createLot({ from: random1, }); }); it(printTitle('random address', 'cannot create a lot while lot creation is disabled'), async () => { // Disable lot creation await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.create.enabled', false, { from: owner }); // Attempt to create lot await shouldRevert(createLot({ from: random1, }), 'Created a lot while lot creation was disabled'); }); it(printTitle('auction lot', 'has correct price at block'), async () => { // Set lot settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.duration', 100000, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.price.start', '1'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.price.reserve', '0.5'.ether, { from: owner }); // Set RPL price let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; await submitPrices(block, slotTimestamp, '1'.ether, { from: trustedNode }); // Create lot await auctionCreateLot({ from: random1 }); // Get lot start block const startBlock = parseInt(await getLotStartBlock(0)); // Set expected prices at blocks const values = [ { block: startBlock + 0, expectedPrice: '1.00000'.ether }, { block: startBlock + 12000, expectedPrice: '0.99280'.ether }, { block: startBlock + 25000, expectedPrice: '0.96875'.ether }, { block: startBlock + 37000, expectedPrice: '0.93155'.ether }, { block: startBlock + 50000, expectedPrice: '0.87500'.ether }, { block: startBlock + 63000, expectedPrice: '0.80155'.ether }, { block: startBlock + 75000, expectedPrice: '0.71875'.ether }, { block: startBlock + 88000, expectedPrice: '0.61280'.ether }, { block: startBlock + 100000, expectedPrice: '0.50000'.ether }, ]; // Check fees for (let vi = 0; vi < values.length; ++vi) { let v = values[vi]; let price = await getLotPriceAtBlock(0, v.block); assertBN.equal(price, v.expectedPrice, 'Lot price does not match expected price at block'); } }); it(printTitle('random address', 'can place a bid on a lot'), async () => { // Create lots await auctionCreateLot({ from: random1 }); await auctionCreateLot({ from: random1 }); // Place bid on first lot from first address await placeBid(0, { from: random1, value: '4'.ether, }); // Increase bid on first lot from first address await placeBid(0, { from: random1, value: '4'.ether, }); // Place bid on first lot from second address await placeBid(0, { from: random2, value: '4'.ether, }); // Place bid on second lot from first address await placeBid(1, { from: random1, value: '2'.ether, }); // Increase bid on second lot from first address await placeBid(1, { from: random1, value: '2'.ether, }); // Place bid on second lot from second address await placeBid(1, { from: random2, value: '2'.ether, }); }); it(printTitle('random address', 'can claim RPL from a lot'), async () => { // Create lots & place bids to clear await auctionCreateLot({ from: random1 }); await auctionCreateLot({ from: random1 }); await auctionPlaceBid(0, { from: random1, value: '5'.ether }); await auctionPlaceBid(0, { from: random2, value: '5'.ether }); await auctionPlaceBid(1, { from: random1, value: '3'.ether }); await auctionPlaceBid(1, { from: random2, value: '3'.ether }); // Claim RPL on first lot from first address await claimBid(0, { from: random1, }); // Claim RPL on first lot from second address await claimBid(0, { from: random2, }); // Claim RPL on second lot from first address await claimBid(1, { from: random1, }); // Claim RPL on second lot from second address await claimBid(1, { from: random2, }); }); it(printTitle('random address', 'can recover unclaimed RPL from a lot'), async () => { // Create closed lots await auctionCreateLot({ from: random1 }); await auctionCreateLot({ from: random1 }); // Wait for duration to end await helpers.mine(auctionDuration); // Recover RPL from first lot await recoverUnclaimedRPL(0, { from: random1, }); // Recover RPL from second lot await recoverUnclaimedRPL(1, { from: random1, }); }); describe('With Lot', () => { before(async () => { // Create lot await auctionCreateLot({ from: random1 }); }); it(printTitle('random address', 'cannot recover unclaimed RPL from a lot which has no RPL to recover'), async () => { await auctionPlaceBid(0, { from: random1, value: '1000'.ether }); // Wait for duration to end await helpers.mine(auctionDuration); // Attempt to recover RPL again await shouldRevert(recoverUnclaimedRPL(0, { from: random1, }), 'Recovered unclaimed RPL from a lot which had no RPL to recover'); }); it(printTitle('random address', 'cannot bid on a lot which doesn\'t exist'), async () => { // Attempt to place bid await shouldRevert(placeBid(1, { from: random1, value: '4'.ether, }), 'Bid on a lot which doesn\'t exist'); }); it(printTitle('random address', 'cannot bid on a lot while bidding is disabled'), async () => { // Disable bidding await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.bidding.enabled', false, { from: owner }); // Attempt to place bid await shouldRevert(placeBid(0, { from: random1, value: '4'.ether, }), 'Bid on a lot while bidding was disabled'); }); it(printTitle('random address', 'cannot bid an invalid amount on a lot'), async () => { // Attempt to place bid await shouldRevert(placeBid(0, { from: random1, value: '0'.ether, }), 'Bid an invalid amount on a lot'); }); it(printTitle('random address', 'cannot bid on a lot after the lot bidding period has concluded'), async () => { // Wait for duration to end await helpers.mine(auctionDuration); // Attempt to place bid await shouldRevert(placeBid(0, { from: random1, value: '4'.ether, }), 'Bid on a lot after the bidding period concluded'); }); it(printTitle('random address', 'cannot bid on a lot after the RPL allocation has been exhausted'), async () => { // Place bid & claim all RPL await placeBid(0, { from: random1, value: '1000'.ether, }); // Attempt to place bid await shouldRevert(placeBid(0, { from: random2, value: '4'.ether, }), 'Bid on a lot after the RPL allocation was exhausted'); }); it(printTitle('random address', 'cannot claim RPL from a lot which doesn\'t exist'), async () => { await auctionPlaceBid(0, { from: random1, value: '1000'.ether }); // Attempt to claim RPL await shouldRevert(claimBid(1, { from: random1, }), 'Claimed RPL from a lot which doesn\'t exist'); }); it(printTitle('random address', 'cannot claim RPL from a lot before it has cleared'), async () => { await auctionPlaceBid(0, { from: random1, value: '4'.ether }); // Attempt to claim RPL await shouldRevert(claimBid(0, { from: random1, }), 'Claimed RPL from a lot before it has cleared'); }); it(printTitle('random address', 'cannot claim RPL from a lot it has not bid on'), async () => { await auctionPlaceBid(0, { from: random1, value: '1000'.ether }); // Attempt to claim RPL await shouldRevert(claimBid(0, { from: random2, }), 'Address claimed RPL from a lot it has not bid on'); }); it(printTitle('random address', 'cannot recover unclaimed RPL from a lot which doesn\'t exist'), async () => { // Wait for duration to end await helpers.mine(auctionDuration); // Attempt to recover RPL await shouldRevert(recoverUnclaimedRPL(1, { from: random1, }), 'Recovered unclaimed RPL from a lot which doesn\'t exist'); }); it(printTitle('random address', 'cannot recover unclaimed RPL from a lot before the lot bidding period has concluded'), async () => { // Attempt to recover RPL await shouldRevert(recoverUnclaimedRPL(0, { from: random1, }), 'Recovered unclaimed RPL from a lot before its bidding period had concluded'); }); it(printTitle('random address', 'cannot recover unclaimed RPL from a lot twice'), async () => { // Wait for duration to end await helpers.mine(auctionDuration); // Recover RPL await recoverUnclaimedRPL(0, { from: random1 }); // Attempt to recover RPL again await shouldRevert(recoverUnclaimedRPL(0, { from: random1, }), 'Recovered unclaimed RPL from a lot twice'); }); }); }); }); } ================================================ FILE: test/auction/scenario-claim-bid.js ================================================ import { RocketAuctionManager, RocketTokenRPL, RocketVault } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Claim RPL from a lot export async function claimBid(lotIndex, txOptions) { // Load contracts const [ rocketAuctionManager, rocketTokenRPL, rocketVault, ] = await Promise.all([ RocketAuctionManager.deployed(), RocketTokenRPL.deployed(), RocketVault.deployed(), ]); // Get auction contract details function getContractDetails() { return Promise.all([ rocketAuctionManager.getAllottedRPLBalance(), rocketAuctionManager.getRemainingRPLBalance(), ]).then( ([allottedRplBalance, remainingRplBalance]) => ({ allottedRplBalance, remainingRplBalance }), ); } // Get lot details function getLotDetails(bidderAddress) { return Promise.all([ rocketAuctionManager.getLotAddressBidAmount(lotIndex, bidderAddress), rocketAuctionManager.getLotCurrentPrice(lotIndex), ]).then( ([addressBidAmount, currentPrice]) => ({ addressBidAmount, currentPrice }), ); } // Get balances function getBalances(bidderAddress) { return Promise.all([ rocketTokenRPL.balanceOf(bidderAddress), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketAuctionManager', rocketTokenRPL.target), ]).then( ([bidderRpl, vaultRpl, contractRpl]) => ({ bidderRpl, vaultRpl, contractRpl }), ); } // Get initial details & balances let [details1, lot1, balances1] = await Promise.all([ getContractDetails(), getLotDetails(txOptions.from), getBalances(txOptions.from), ]); // Claim RPL await rocketAuctionManager.connect(txOptions.from).claimBid(lotIndex, txOptions); // Get updated details & balances let [details2, lot2, balances2] = await Promise.all([ getContractDetails(), getLotDetails(txOptions.from), getBalances(txOptions.from), ]); // Get expected values const calcBase = '1'.ether; const expectedRplAmount = calcBase * lot1.addressBidAmount / lot1.currentPrice; // Check details assertBN.equal(details2.allottedRplBalance, details1.allottedRplBalance - expectedRplAmount, 'Incorrect updated contract allotted RPL balance'); assertBN.equal(details2.remainingRplBalance, details1.remainingRplBalance, 'Contract remaining RPL balance updated and should not have'); assertBN.equal(lot2.addressBidAmount, 0, 'Incorrect updated address bid amount'); // Check balances assertBN.equal(balances2.bidderRpl, balances1.bidderRpl + expectedRplAmount, 'Incorrect updated address RPL balance'); assertBN.equal(balances2.contractRpl, balances1.contractRpl - expectedRplAmount, 'Incorrect updated auction contract RPL balance'); assertBN.equal(balances2.vaultRpl, balances1.vaultRpl - expectedRplAmount, 'Incorrect updated vault RPL balance'); } ================================================ FILE: test/auction/scenario-create-lot.js ================================================ import { RocketAuctionManager, RocketDAOProtocolSettingsAuction, RocketNetworkPrices } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Create a new lot for auction export async function createLot(txOptions) { // Load contracts const [ rocketAuctionManager, rocketAuctionSettings, rocketNetworkPrices, ] = await Promise.all([ RocketAuctionManager.deployed(), RocketDAOProtocolSettingsAuction.deployed(), RocketNetworkPrices.deployed(), ]); // Get parameters const [ lotMaxEthValue, lotDuration, startPriceRatio, reservePriceRatio, rplPrice, ] = await Promise.all([ rocketAuctionSettings.getLotMaximumEthValue(), rocketAuctionSettings.getLotDuration(), rocketAuctionSettings.getStartingPriceRatio(), rocketAuctionSettings.getReservePriceRatio(), rocketNetworkPrices.getRPLPrice(), ]); // Get auction contract details function getContractDetails() { return Promise.all([ rocketAuctionManager.getTotalRPLBalance(), rocketAuctionManager.getAllottedRPLBalance(), rocketAuctionManager.getRemainingRPLBalance(), rocketAuctionManager.getLotCount(), ]).then( ([totalRplBalance, allottedRplBalance, remainingRplBalance, lotCount]) => ({ totalRplBalance, allottedRplBalance, remainingRplBalance, lotCount }), ); } // Get lot details function getLotDetails(lotIndex) { return Promise.all([ rocketAuctionManager.getLotExists(lotIndex), rocketAuctionManager.getLotStartBlock(lotIndex), rocketAuctionManager.getLotEndBlock(lotIndex), rocketAuctionManager.getLotStartPrice(lotIndex), rocketAuctionManager.getLotReservePrice(lotIndex), rocketAuctionManager.getLotTotalRPLAmount(lotIndex), rocketAuctionManager.getLotCurrentPrice(lotIndex), rocketAuctionManager.getLotClaimedRPLAmount(lotIndex), rocketAuctionManager.getLotRemainingRPLAmount(lotIndex), rocketAuctionManager.getLotIsCleared(lotIndex), ]).then( ([exists, startBlock, endBlock, startPrice, reservePrice, totalRpl, currentPrice, claimedRpl, remainingRpl, isCleared]) => ({ exists, startBlock, endBlock, startPrice, reservePrice, totalRpl, currentPrice, claimedRpl, remainingRpl, isCleared, }), ); } // Get initial contract details let details1 = await getContractDetails(); // Create lot await rocketAuctionManager.connect(txOptions.from).createLot(txOptions); // Get updated contract details let [details2, lot] = await Promise.all([ getContractDetails(), getLotDetails(details1.lotCount), ]); // Get expected values const calcBase = '1'.ether; const lotMaxRplAmount = calcBase * lotMaxEthValue / rplPrice; const expectedRemainingRplBalance = (details1.remainingRplBalance > lotMaxRplAmount) ? (details1.remainingRplBalance - lotMaxRplAmount) : '0'.ether; const expectedLotRplAmount = (details1.remainingRplBalance < lotMaxRplAmount) ? details1.remainingRplBalance : lotMaxRplAmount; // Check contract details assertBN.equal(details2.totalRplBalance, details1.totalRplBalance, 'Total RPL balance updated and should not have'); assertBN.equal(details2.remainingRplBalance, expectedRemainingRplBalance, 'Incorrect updated remaining RPL balance'); assertBN.equal(details2.totalRplBalance, details2.allottedRplBalance + details2.remainingRplBalance, 'Incorrect updated RPL balances'); assertBN.equal(details2.lotCount, details1.lotCount + 1n, 'Incorrect updated lot count'); // Check lot details assert.equal(lot.exists, true, 'Incorrect lot exists status'); assertBN.equal(lot.endBlock, lot.startBlock + lotDuration, 'Incorrect lot start/end blocks'); assertBN.equal(lot.startPrice, rplPrice * startPriceRatio / calcBase, 'Incorrect lot starting price'); assertBN.equal(lot.reservePrice, rplPrice * reservePriceRatio / calcBase, 'Incorrect lot reserve price'); assertBN.equal(lot.totalRpl, expectedLotRplAmount, 'Incorrect lot total RPL amount'); assertBN.equal(lot.currentPrice, lot.startPrice, 'Incorrect lot current price'); assertBN.equal(lot.claimedRpl, 0, 'Incorrect lot claimed RPL amount'); assertBN.equal(lot.remainingRpl, lot.totalRpl, 'Incorrect lot remaining RPL amount'); assert.equal(lot.isCleared, false, 'Incorrect lot cleared status'); } ================================================ FILE: test/auction/scenario-place-bid.js ================================================ import { RocketAuctionManager, RocketVault } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Place a bid on a lot export async function placeBid(lotIndex, txOptions) { // Load contracts const [ rocketAuctionManager, rocketVault, ] = await Promise.all([ RocketAuctionManager.deployed(), RocketVault.deployed(), ]); // Calculation base value const calcBase = '1'.ether; // Get lot details function getLotDetails(bidderAddress) { return Promise.all([ rocketAuctionManager.getLotTotalRPLAmount(lotIndex), rocketAuctionManager.getLotTotalBidAmount(lotIndex), rocketAuctionManager.getLotAddressBidAmount(lotIndex, bidderAddress), rocketAuctionManager.getLotPriceByTotalBids(lotIndex), rocketAuctionManager.getLotCurrentPrice(lotIndex), rocketAuctionManager.getLotClaimedRPLAmount(lotIndex), rocketAuctionManager.getLotRemainingRPLAmount(lotIndex), ]).then( ([totalRplAmount, totalBidAmount, addressBidAmount, priceByTotalBids, currentPrice, claimedRplAmount, remainingRplAmount]) => ({ totalRplAmount, totalBidAmount, addressBidAmount, priceByTotalBids, currentPrice, claimedRplAmount, remainingRplAmount, }), ); } // Get balances function getBalances(bidderAddress) { return Promise.all([ ethers.provider.getBalance(bidderAddress), ethers.provider.getBalance(rocketVault.target), rocketVault.balanceOf('rocketDepositPool'), ]).then( ([bidderEth, vaultEth, depositPoolEth]) => ({ bidderEth, vaultEth, depositPoolEth }), ); } // Get lot price at block function getLotPriceAtBlock() { return ethers.provider.getBlock('latest') .then(block => rocketAuctionManager.getLotPriceAtBlock(lotIndex, block.number)); } // Get initial lot details & balances let [lot1, balances1] = await Promise.all([ getLotDetails(txOptions.from), getBalances(txOptions.from), ]); // Set gas price let gasPrice = '20'.gwei; txOptions.gasPrice = gasPrice; // Place bid let tx = await rocketAuctionManager.connect(txOptions.from).placeBid(lotIndex, txOptions); let txReceipt = await tx.wait(); let txFee = gasPrice * txReceipt.gasUsed; // Get updated lot details & balances let [lot2, balances2] = await Promise.all([ getLotDetails(txOptions.from), getBalances(txOptions.from), ]); // Get parameters const lotBlockPrice = await getLotPriceAtBlock(); const lotRemainingRplAmount = lot1.totalRplAmount - (calcBase * lot1.totalBidAmount / lotBlockPrice); // Get expected values const maxBidAmount = lotRemainingRplAmount * lotBlockPrice / calcBase; const txValue = txOptions.value; const bidAmount = (txValue > maxBidAmount) ? maxBidAmount : txValue; // Check lot details assertBN.equal(lot2.totalBidAmount, lot1.totalBidAmount + bidAmount, 'Incorrect updated total bid amount'); assertBN.equal(lot2.addressBidAmount, lot1.addressBidAmount + bidAmount, 'Incorrect updated address bid amount'); assertBN.equal(lot2.priceByTotalBids, calcBase * lot2.totalBidAmount / lot2.totalRplAmount, 'Incorrect updated price by total bids'); assertBN.equal(lot2.claimedRplAmount, calcBase * lot2.totalBidAmount / lot2.currentPrice, 'Incorrect updated claimed RPL amount'); assertBN.equal(lot2.totalRplAmount, lot2.claimedRplAmount + lot2.remainingRplAmount, 'Incorrect updated RPL amounts'); // Check balances assertBN.equal(balances2.bidderEth, balances1.bidderEth - bidAmount - txFee, 'Incorrect updated address ETH balance'); assertBN.equal(balances2.depositPoolEth, balances1.depositPoolEth + bidAmount, 'Incorrect updated deposit pool ETH balance'); assertBN.equal(balances2.vaultEth, balances1.vaultEth + bidAmount, 'Incorrect updated vault ETH balance'); } ================================================ FILE: test/auction/scenario-recover-rpl.js ================================================ import { RocketAuctionManager } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Recover unclaimed RPL from a lot export async function recoverUnclaimedRPL(lotIndex, txOptions) { // Load contracts const rocketAuctionManager = await RocketAuctionManager.deployed(); // Get auction contract details function getContractDetails() { return Promise.all([ rocketAuctionManager.getAllottedRPLBalance(), rocketAuctionManager.getRemainingRPLBalance(), ]).then( ([allottedRplBalance, remainingRplBalance]) => ({ allottedRplBalance, remainingRplBalance }), ); } // Get lot details function getLotDetails() { return Promise.all([ rocketAuctionManager.getLotRPLRecovered(lotIndex), rocketAuctionManager.getLotRemainingRPLAmount(lotIndex), ]).then( ([rplRecovered, remainingRplAmount]) => ({ rplRecovered, remainingRplAmount }), ); } // Get initial details let [details1, lot1] = await Promise.all([ getContractDetails(), getLotDetails(), ]); // Recover RPL await rocketAuctionManager.connect(txOptions.from).recoverUnclaimedRPL(lotIndex, txOptions); // Get updated details let [details2, lot2] = await Promise.all([ getContractDetails(), getLotDetails(), ]); // Check details assertBN.equal(details2.allottedRplBalance, details1.allottedRplBalance - lot1.remainingRplAmount, 'Incorrect updated contract allotted RPL balance'); assertBN.equal(details2.remainingRplBalance, details1.remainingRplBalance + lot1.remainingRplAmount, 'Incorrect updated contract remaining RPL balance'); assert.equal(lot2.rplRecovered, true, 'Incorrect updated lot RPL recovered status'); } ================================================ FILE: test/dao/dao-node-trusted-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { compressABI } from '../_utils/contract'; import { registerNode, setNodeTrusted } from '../_helpers/node'; import { mintDummyRPL } from '../token/scenario-rpl-mint-fixed'; import { burnFixedRPL } from '../token/scenario-rpl-burn-fixed'; import { allowDummyRPL } from '../token/scenario-rpl-allow-fixed'; import { setDaoNodeTrustedBootstrapMember, setDaoNodeTrustedBootstrapModeDisabled, setDAONodeTrustedBootstrapSetting, setDaoNodeTrustedBootstrapUpgrade, setDaoNodeTrustedMemberRequired, } from './scenario-dao-node-trusted-bootstrap'; import { daoNodeTrustedCancel, daoNodeTrustedExecute, daoNodeTrustedMemberChallengeDecide, daoNodeTrustedMemberChallengeMake, daoNodeTrustedMemberJoin, daoNodeTrustedMemberLeave, daoNodeTrustedPropose, daoNodeTrustedVote, getDAOMemberIsValid, } from './scenario-dao-node-trusted'; import { getDAOProposalEndTime, getDAOProposalExpires, getDAOProposalStartTime, getDAOProposalState, proposalStates, } from './scenario-dao-proposal'; import { assertBN } from '../_helpers/bn'; import { RocketDAONodeTrusted, RocketDAONodeTrustedActions, RocketDAONodeTrustedProposals, RocketDAONodeTrustedSettingsMembers, RocketDAONodeTrustedSettingsProposals, RocketDAONodeTrustedUpgrade, RocketDAOProtocolSettingsMegapool, RocketDAOProtocolSettingsNode, RocketDAOProtocolSettingsSecurity, RocketMinipoolManager, RocketStorage, RocketTokenRPL, } from '../_utils/artifacts'; import * as assert from 'assert'; import { globalSnapShot } from '../_utils/snapshotting'; import { setDAOProtocolBootstrapSetting } from './scenario-dao-protocol-bootstrap'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketDAONodeTrusted', () => { let guardian, userOne, registeredNode1, registeredNode2, registeredNode3, registeredNodeTrusted1, registeredNodeTrusted2, registeredNodeTrusted3; // Mints fixed supply RPL, burns that for new RPL and gives it to the account let rplMint = async function(_account, _amount) { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // Mint RPL fixed supply for the users to simulate current users having RPL await mintDummyRPL(_account, _amount, { from: guardian }); // Mint a large amount of dummy RPL to guardian, who then burns it for real RPL which is sent to nodes for testing below await allowDummyRPL(rocketTokenRPL.target, _amount, { from: _account }); // Burn existing fixed supply RPL for new RPL await burnFixedRPL(_amount, { from: _account }); }; // Allow the given account to spend this users RPL let rplAllowanceDAO = async function(_account, _amount) { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); // Approve now await rocketTokenRPL.connect(_account).approve(rocketDAONodeTrustedActions.target, _amount, { from: _account }); }; // Add a new DAO member via bootstrap mode let bootstrapMemberAdd = async function(_account, _id, _url) { // Use helper now await setNodeTrusted(_account, _id, _url, guardian); }; // Setup let rocketMinipoolManagerNew; let rocketDAONodeTrustedUpgradeNew; const upgradeDelay = 60n * 60n * 24n; // 1 day before(async () => { await globalSnapShot(); [ guardian, userOne, registeredNode1, registeredNode2, registeredNode3, registeredNodeTrusted1, registeredNodeTrusted2, registeredNodeTrusted3, ] = await ethers.getSigners(); // Get RocketStorage const rocketStorage = await RocketStorage.deployed(); // Register nodes await registerNode({ from: registeredNode1 }); await registerNode({ from: registeredNode2 }); await registerNode({ from: registeredNode3 }); await registerNode({ from: registeredNodeTrusted1 }); await registerNode({ from: registeredNodeTrusted2 }); await registerNode({ from: registeredNodeTrusted3 }); // Add members to the DAO now await bootstrapMemberAdd(registeredNodeTrusted1, 'rocketpool_1', 'node@home.com'); await bootstrapMemberAdd(registeredNodeTrusted2, 'rocketpool_2', 'node@home.com'); // Deploy new contracts rocketMinipoolManagerNew = await RocketMinipoolManager.clone(rocketStorage.target); rocketDAONodeTrustedUpgradeNew = await RocketDAONodeTrustedUpgrade.clone(rocketStorage.target); // Set a small proposal cooldown await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown', 10, { from: guardian }); // Set a small vote delay await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.delay.blocks', 4, { from: guardian }); // Set upgrade delay await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsSecurity, 'upgrade.delay', upgradeDelay, { from: guardian }); }); // // Start Tests // it(printTitle('userOne', 'fails to be added as a trusted node dao member as they are not a registered node'), async () => { // Set as trusted dao member via bootstrapping await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', userOne, { from: guardian, }), 'Non registered node added to trusted node DAO', 'Invalid node'); }); it(printTitle('userOne', 'fails to add a bootstrap trusted node DAO member as non guardian'), async () => { // Set as trusted dao member via bootstrapping await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', registeredNode1, { from: userOne, }), 'Non guardian registered node to trusted node DAO', 'Account is not a temporary guardian'); }); it(printTitle('guardian', 'cannot add the same member twice'), async () => { // Set as trusted dao member via bootstrapping await shouldRevert(setDaoNodeTrustedBootstrapMember('rocketpool', 'node@home.com', registeredNodeTrusted2, { from: guardian, }), 'Guardian the same DAO member twice', 'This node is already part of the trusted node DAO'); }); it(printTitle('guardian', 'updates quorum setting while bootstrap mode is enabled'), async () => { // Set as trusted dao member via bootstrapping await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', '0.55'.ether, { from: guardian, }); }); it(printTitle('guardian', 'updates RPL bond setting while bootstrap mode is enabled'), async () => { // Set RPL Bond at 10K RPL await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.rplbond', '10000'.ether, { from: guardian, }); }); it(printTitle('userOne', 'fails to update RPL bond setting while bootstrap mode is enabled as they are not the guardian'), async () => { // Update setting await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.rplbond', '10000'.ether, { from: userOne, }), 'UserOne changed RPL bond setting', 'Account is not a temporary guardian'); }); it(printTitle('guardian', 'fails to update setting after bootstrap mode is disabled'), async () => { // Disable bootstrap mode await setDaoNodeTrustedBootstrapModeDisabled({ from: guardian, }); // Update setting await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'members.quorum', '0.55'.ether, { from: guardian, }), 'Guardian updated setting after bootstrap mode is disabled', 'Bootstrap mode not engaged'); }); it(printTitle('guardian', 'fails to set quorum setting as 0% while bootstrap mode is enabled'), async () => { // Update setting await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', 0n, { from: guardian, }), 'Guardian changed quorum setting to invalid value', 'Quorum setting must be > 0 & <= 90%'); }); it(printTitle('guardian', 'fails to set quorum setting above 90% while bootstrap mode is enabled'), async () => { // Update setting await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.quorum', '0.91'.ether, { from: guardian, }), 'Guardian changed quorum setting to invalid value', 'Quorum setting must be > 0 & <= 90%'); }); it(printTitle('registeredNode1', 'verify trusted node quorum votes required is correct'), async () => { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedSettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How many trusted nodes do we have? let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); // Get the current quorum threshold let quorumThreshold = await rocketDAONodeTrustedSettings.getQuorum(); // Calculate the expected vote threshold let expectedVotes = quorumThreshold * trustedNodeCount; // Calculate it now on the contracts let quorumVotes = await rocketDAONodeTrusted.getMemberQuorumVotesRequired(); // Verify assertBN.equal(expectedVotes, quorumVotes, 'Expected vote threshold does not match contracts'); }); // The big test it(printTitle('registeredNodeTrusted1&2', 'create two proposals for two new members that are voted in, one then chooses to leave and is allowed too'), async () => { // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // How much RPL is required for a trusted node bond? let rplBondAmount = await daoNodesettings.getRPLBond(); // Disable bootstrap mode await setDaoNodeTrustedBootstrapModeDisabled({ from: guardian }); // We only have 2 members now that bootstrap mode is disabled and proposals can only be made with 3, lets get a regular node to join via the emergency method // We'll allow the DAO to transfer our RPL bond before joining await rplMint(registeredNode3, rplBondAmount); await rplAllowanceDAO(registeredNode3, rplBondAmount); await setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node3@home.com', { from: registeredNode3, }); // New Member 1 // Encode the calldata for the proposal let proposalCalldata1 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider1', 'test@sass.com', registeredNode1.address]); // Add the proposal let proposalID_1 = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata1, { from: registeredNodeTrusted1, }); // New Member 2 // Encode the calldata for the proposal let proposalCalldata2 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider2', 'test@sass.com', registeredNode2.address]); // Add the proposal let proposalID_2 = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata2, { from: registeredNodeTrusted2, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID_1) - timeCurrent) + 2); // Now lets vote for the new members await daoNodeTrustedVote(proposalID_1, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID_1, true, { from: registeredNodeTrusted2 }); await daoNodeTrustedVote(proposalID_2, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID_2, true, { from: registeredNodeTrusted2 }); // Current time timeCurrent = await helpers.time.latest(); // Fast forward to voting periods finishing await helpers.time.increase((await getDAOProposalEndTime(proposalID_1) - timeCurrent) + 2); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalID_1, { from: registeredNodeTrusted1 }); await daoNodeTrustedExecute(proposalID_2, { from: registeredNodeTrusted1 }); // Member has now been invited to join, so lets do that // We'll allow the DAO to transfer our RPL bond before joining await rplMint(registeredNode1, rplBondAmount); await rplAllowanceDAO(registeredNode1, rplBondAmount); await rplMint(registeredNode2, rplBondAmount); await rplAllowanceDAO(registeredNode2, rplBondAmount); // Join now await daoNodeTrustedMemberJoin({ from: registeredNode1 }); await daoNodeTrustedMemberJoin({ from: registeredNode2 }); // Add a small wait between member join and proposal await helpers.time.increase(2); // Now registeredNodeTrusted2 wants to leave // Encode the calldata for the proposal let proposalCalldata3 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [registeredNodeTrusted2.address]); // Add the proposal let proposalID_3 = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata3, { from: registeredNodeTrusted2, }); // Current time timeCurrent = await helpers.time.latest(); // Now mine blocks until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID_3) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID_3, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID_3, true, { from: registeredNodeTrusted2 }); await daoNodeTrustedVote(proposalID_3, false, { from: registeredNode1 }); await daoNodeTrustedVote(proposalID_3, true, { from: registeredNode2 }); // Current time timeCurrent = await helpers.time.latest(); // Fast forward to this voting period finishing await helpers.time.increase((await getDAOProposalEndTime(proposalID_3) - timeCurrent) + 2); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalID_3, { from: registeredNodeTrusted2 }); // Member can now leave and collect any RPL bond await daoNodeTrustedMemberLeave(registeredNodeTrusted2, { from: registeredNodeTrusted2 }); }); // Test various proposal states it(printTitle('registeredNodeTrusted1', 'creates a proposal and verifies the proposal states as it passes and is executed'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com'); await helpers.time.increase(60); // Now registeredNodeTrusted2 wants to leave // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider1', 'test@sass.com', registeredNode2.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, { from: registeredNodeTrusted1, }); // Verify the proposal is pending assert.equal(await getDAOProposalState(proposalID), proposalStates.Pending, 'Proposal state is not Pending'); // Verify voting will not work while pending await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }), 'Member voted while proposal was pending', 'Voting is not active for this proposal'); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); await shouldRevert(daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted1 }), 'Member voted after proposal has passed', 'Proposal has passed, voting is complete and the proposal can now be executed'); // Verify the proposal is successful assert.equal(await getDAOProposalState(proposalID), proposalStates.Succeeded, 'Proposal state is not succeeded'); // Proposal has passed, lets execute it now await daoNodeTrustedExecute(proposalID, { from: registeredNode1 }); // Verify the proposal has executed assert.equal(await getDAOProposalState(proposalID), proposalStates.Executed, 'Proposal state is not executed'); }); // Test various proposal states it(printTitle('registeredNodeTrusted1', 'creates a proposal and verifies the proposal states as it fails after it expires'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com'); await helpers.time.increase(60); // Now registeredNodeTrusted2 wants to leave // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider1', 'test@sass.com', registeredNode2.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, { from: registeredNodeTrusted1, }); // Verify the proposal is pending assert.equal(await getDAOProposalState(proposalID), proposalStates.Pending, 'Proposal state is not Pending'); // Verify voting will not work while pending await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }), 'Member voted while proposal was pending', 'Voting is not active for this proposal'); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }); await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted2 }); await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted1 }); // Fast forward to this voting period finishing await helpers.time.increase((await getDAOProposalEndTime(proposalID) - timeCurrent) + 2); // Verify the proposal is defeated assert.equal(await getDAOProposalState(proposalID), proposalStates.Defeated, 'Proposal state is not defeated'); // Proposal has failed, can we execute it anyway? await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode1 }), 'Executed defeated proposal', 'Proposal has not succeeded, has expired or has already been executed'); ; }); it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member but cancels it before it passes'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider1', 'test@sass.com', registeredNode1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); // Cancel now before it passes await daoNodeTrustedCancel(proposalID, { from: registeredNodeTrusted1 }); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member, then attempts to again for registeredNode2 before cooldown has passed and that fails'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Setup our proposal settings let proposalCooldownTime = 60 * 60; // Update now while in bootstrap mode await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown.time', proposalCooldownTime, { from: guardian }); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider', 'test@sass.com', registeredNode1.address]); // Add the proposal await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, { from: registeredNodeTrusted1, }); // Encode the calldata for the proposal let proposalCalldata2 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider2', 'test2@sass.com', registeredNode2.address]); // Add the proposal await shouldRevert(daoNodeTrustedPropose('hey guys, can we add this other cool SaaS member please?', proposalCalldata2, { from: registeredNodeTrusted1, }), 'Add proposal before cooldown period passed', 'Member has not waited long enough to make another proposal'); // Current block let timeCurrent = await helpers.time.latest(); // Now wait until the cooldown period expires and proposal can be made again await helpers.time.increase(timeCurrent + proposalCooldownTime + 2); // Try again await daoNodeTrustedPropose('hey guys, can we add this other cool SaaS member please?', proposalCalldata2, { from: registeredNodeTrusted1, }); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member, registeredNode2 tries to vote on it, but fails as they joined after it was created'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider', 'test@sass.com', registeredNode1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata, { from: registeredNodeTrusted1, }); // Now add a new member after that proposal was created await bootstrapMemberAdd(registeredNode2, 'rocketpool', 'node@home.com'); // Current block let timeCurrent = await helpers.time.latest(); // Now wait until the cooldown period expires and proposal can be made again await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // registeredNodeTrusted1 votes await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); // registeredNode2 vote fails await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode2, }), 'Voted on proposal created before they joined', 'Member cannot vote on proposal created before they became a member'); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal to leave the DAO and receive their RPL bond refund, proposal is denied as it would be under the min members required for the DAO'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [registeredNodeTrusted1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); // Fast forward to this voting period finishing await helpers.time.increase((await getDAOProposalEndTime(proposalID) - timeCurrent) + 2); // Proposal should be successful, lets execute it await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member proposal successful to leave DAO when they shouldnt be able too', 'Member count will fall below min required'); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal to kick registeredNodeTrusted2 with a 50% fine, it is successful and registeredNodeTrusted2 is kicked and receives 50% of their bond'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Get the DAO settings const daoNode = await RocketDAONodeTrusted.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); // Add our 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool', 'node@home.com'); await helpers.time.increase(60); // How much bond has registeredNodeTrusted2 paid? let registeredNodeTrusted2BondAmount = await daoNode.getMemberRPLBondAmount(registeredNodeTrusted2); // How much to fine? 33% let registeredNodeTrusted2BondAmountFine = registeredNodeTrusted2BondAmount / 3n; // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalKick', [registeredNodeTrusted2.address, registeredNodeTrusted2BondAmountFine]); // Get the RPL total supply let rplTotalSupply1 = await rocketTokenRPL.totalSupply.call(); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, this member hasn\'t logged on for weeks, lets boot them with a 33% fine!', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNode1 }); await daoNodeTrustedVote(proposalID, false, { from: registeredNodeTrusted2 }); // Don't kick me await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted3 }); // Proposal has passed, lets execute it now await daoNodeTrustedExecute(proposalID, { from: registeredNode1 }); // Member should be kicked now, let's check their RPL balance has their 33% bond returned let rplBalance = await rocketTokenRPL.balanceOf(registeredNodeTrusted2); assertBN.equal((registeredNodeTrusted2BondAmount - registeredNodeTrusted2BondAmountFine), rplBalance, 'registeredNodeTrusted2 remaining RPL balance is incorrect'); assert.equal(await getDAOMemberIsValid(registeredNodeTrusted2), false, 'registeredNodeTrusted2 is still a member of the DAO'); // The 33% fine should be burned let rplTotalSupply2 = await rocketTokenRPL.totalSupply(); assertBN.equal(rplTotalSupply1 - rplTotalSupply2, registeredNodeTrusted2BondAmountFine, 'RPL total supply did not decrease by fine amount'); }); it(printTitle('registeredNode2', 'is made a new member after a proposal is created, they fail to vote on that proposal'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [registeredNodeTrusted1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: registeredNodeTrusted1, }); // Register new member now await bootstrapMemberAdd(registeredNode2, 'rocketpool', 'node@home.com'); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); // New member attempts to vote on proposal started before they joined, fails await shouldRevert(daoNodeTrustedVote(proposalID, true, { from: registeredNode2 }), 'Member voted on proposal they shouldn\'t be able too', 'Member cannot vote on proposal created before they became a member'); }); it(printTitle('registeredNodeTrusted2', 'fails to execute a successful proposal after it expires'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [registeredNodeTrusted1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); // Fast forward to this voting period finishing and executing period expiring await helpers.time.increase((await getDAOProposalExpires(proposalID) - timeCurrent) + 2); // Verify correct expired status assert.equal(await getDAOProposalState(proposalID), proposalStates.Expired, 'Proposal state is not Expired'); // Execution should fail await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member execute proposal after it had expired', 'Proposal has not succeeded, has expired or has already been executed'); }); it(printTitle('registeredNodeTrusted2', 'checks to see if a proposal has expired after being successfully voted for, but not executed'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [registeredNodeTrusted1.address]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); // Fast forward to this voting period finishing and executing period expiring await helpers.time.increase((await getDAOProposalExpires(proposalID) - timeCurrent) + 2); // Execution should fail await shouldRevert(daoNodeTrustedExecute(proposalID, { from: registeredNode2 }), 'Member execute proposal after it had expired', 'Proposal has not succeeded, has expired or has already been executed'); // Cancel should fail await shouldRevert(daoNodeTrustedCancel(proposalID, { from: registeredNodeTrusted1 }), 'Member cancelled proposal after it had expired', 'Proposal can only be cancelled if pending or active'); }); it(printTitle('registeredNodeTrusted1', 'challenges another members node to respond and it does successfully in the window required'), async () => { // Add a 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com'); // Update our challenge settings let challengeWindowTime = 60 * 60; let challengeCooldownTime = 60 * 60; // Update now while in bootstrap mode await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian }); // Attempt to challenge a non-member await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode2, { from: registeredNodeTrusted1 }), 'A non member was challenged', 'Invalid trusted node'); // Challenge the 3rd member await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 }); // Attempt to challenge again await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 }), 'Member was challenged again', 'Member is already being challenged'); // Attempt to challenge another member before cooldown has passed await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNodeTrusted2, { from: registeredNodeTrusted1 }), 'Member challenged another user before cooldown had passed', 'You must wait for the challenge cooldown to pass before issuing another challenge'); // Have 3rd member respond to the challenge successfully await daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNode1 }); // Wait until the original initiator's cooldown window has passed and they attempt another challenge await helpers.time.increase(challengeCooldownTime + 2); await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 }); // Fast forward to past the challenge window with the challenged node responding await helpers.time.increase(challengeWindowTime + 2); // Have 3rd member respond to the challenge successfully again, but after the challenge window has expired and before another member decides it await daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNode1 }); }); it(printTitle('registeredNodeTrusted1', 'challenges another members node to respond, they do not in the window required and lose their membership + bond'), async () => { // Add a 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com'); // Update our challenge settings let challengeWindowTime = 60 * 60; let challengeCooldownTime = 60 * 60; // Update now while in bootstrap mode await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian }); // Try to challenge yourself await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNode1 }), 'Member challenged themselves', 'You cannot challenge yourself'); // Challenge the 3rd member await daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNodeTrusted1 }); // Attempt to decide a challenge on a member that hasn't been challenged await shouldRevert(daoNodeTrustedMemberChallengeDecide(registeredNodeTrusted2, true, { from: registeredNodeTrusted1 }), 'Member decided challenge on member without a challenge', 'Member hasn\'t been challenged or they have successfully responded to the challenge already'); // Have another member try to decide the result before the window passes, it shouldn't change and they should still be a member await shouldRevert(daoNodeTrustedMemberChallengeDecide(registeredNode1, true, { from: registeredNodeTrusted2 }), 'Member decided challenge before refute window passed', 'Refute window has not yet passed'); // Fast forward to past the challenge window with the challenged node responding await helpers.time.increase(challengeWindowTime + 2); // Decide the challenge now after the node hasn't responded in the challenge window await daoNodeTrustedMemberChallengeDecide(registeredNode1, false, { from: registeredNodeTrusted2 }); }); it(printTitle('registeredNode2', 'as a regular node challenges a DAO members node to respond by paying ETH, they do not respond in the window required and lose their membership + bond'), async () => { // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How much ETH is required for a regular node to challenge a DAO member let challengeCost = await daoNodesettings.getChallengeCost(); // Add a 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com'); await helpers.time.increase(60); // Update our challenge settings let challengeWindowTime = 60 * 60; let challengeCooldownTime = 60 * 60; // Update now while in bootstrap mode await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.window', challengeWindowTime, { from: guardian }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMembers, 'members.challenge.cooldown', challengeCooldownTime, { from: guardian }); // Attempt to challenge a non member await shouldRevert(daoNodeTrustedMemberChallengeMake(userOne, { from: registeredNode2, }), 'Challenged a non DAO member', 'Invalid trusted node'); // Attempt to challenge as a non member await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNodeTrusted2, { from: userOne, }), 'Challenged a non DAO member', 'Invalid node'); // Challenge the 3rd member as a regular node, should revert as we haven't paid to challenge await shouldRevert(daoNodeTrustedMemberChallengeMake(registeredNode1, { from: registeredNode2, }), 'Regular node challenged DAO member without paying challenge fee', 'Non DAO members must pay ETH to challenge a members node'); // Ok pay now to challenge await daoNodeTrustedMemberChallengeMake(registeredNode1, { value: challengeCost, from: registeredNode2, }); // Fast forward to past the challenge window with the challenged node responding await helpers.time.increase(challengeWindowTime + 2); // Decide the challenge now after the node hasn't responded in the challenge window await daoNodeTrustedMemberChallengeDecide(registeredNode1, false, { from: registeredNodeTrusted2 }); }); it(printTitle('registered2', 'joins the DAO automatically as a member due to the min number of members falling below the min required'), async () => { // Attempt to join as a non node operator await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', { from: userOne, }), 'Regular node joined DAO without bond during low member mode', 'Invalid node'); // Attempt to join without setting allowance for the bond await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', { from: registeredNode2, }), 'Regular node joined DAO without bond during low member mode', 'Not enough allowance given to RocketDAONodeTrusted contract for transfer of RPL bond tokens'); // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How much RPL is required for a trusted node bond? let rplBondAmount = await daoNodesettings.getRPLBond(); // We'll allow the DAO to transfer our RPL bond before joining await rplMint(registeredNode2, rplBondAmount); await rplAllowanceDAO(registeredNode2, rplBondAmount); // Should just be 2 nodes in the DAO now which means a 3rd can join to make up the min count await setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', { from: registeredNode2, }); }); it(printTitle('registered2', 'attempt to auto join the DAO automatically and fails as the DAO has the min member count required'), async () => { // Add a 3rd member await bootstrapMemberAdd(registeredNode1, 'rocketpool_3', 'node2@home.com'); // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How much RPL is required for a trusted node bond? let rplBondAmount = await daoNodesettings.getRPLBond(); // We'll allow the DAO to transfer our RPL bond before joining await rplMint(registeredNode2, rplBondAmount); await rplAllowanceDAO(registeredNode2, rplBondAmount); // Should just be 2 nodes in the DAO now which means a 3rd can join to make up the min count await shouldRevert(setDaoNodeTrustedMemberRequired('rocketpool_emergency_node_op', 'node2@home.com', { from: registeredNode2, }), 'Regular node joined DAO when not in low member mode', 'Low member mode not engaged'); }); /*** Upgrade Contacts & ABI *************/ // Contracts it(printTitle('guardian', 'can upgrade a contract in bootstrap mode'), async () => { await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }); }); it(printTitle('guardian', 'can upgrade the upgrade contract'), async () => { await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketDAONodeTrustedUpgrade', RocketDAONodeTrustedUpgrade.abi, rocketDAONodeTrustedUpgradeNew.target, { from: guardian, }); }); it(printTitle('userOne', 'cannot upgrade a contract in bootstrap mode'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: userOne, }), 'Random address upgraded a contract', 'Account is not a temporary guardian'); }); it(printTitle('guardian', 'cannot upgrade a contract with an invalid address'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Guardian upgraded a contract with an invalid address', 'Invalid contract address'); }); it(printTitle('guardian', 'cannot upgrade a contract with an existing one'), async () => { const rocketStorageAddress = (await RocketStorage.deployed()).target; await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeManager', [], rocketStorageAddress, { from: guardian, }), 'Guardian upgraded a contract with an existing contract', 'Contract address is already in use'); }); it(printTitle('guardian', 'cannot upgrade a contract with an empty ABI'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketDAONodeTrustedUpgrade', '', rocketDAONodeTrustedUpgradeNew.target, { from: guardian, }), 'Guardian upgraded a contract with an empty ABI', 'Empty ABI is invalid'); }); it(printTitle('guardian', 'cannot upgrade a protected contract'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketVault', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Upgraded a protected contract', 'Cannot upgrade the vault'); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketTokenRETH', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Upgraded a protected contract', 'Cannot upgrade token contracts'); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketTokenRPL', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Upgraded a protected contract', 'Cannot upgrade token contracts'); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketTokenRPLFixedSupply', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Upgraded a protected contract', 'Cannot upgrade token contracts'); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'casperDeposit', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Upgraded a protected contract', 'Cannot upgrade the casper deposit contract'); }); it(printTitle('guardian', 'can add a contract in bootstrap mode'), async () => { await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketMinipoolManagerNew', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }); }); it(printTitle('guardian', 'cannot add a contract with the same name as an existing one'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketStorage', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Guardian added a contract with the same name as an existing one', 'Contract name is already in use'); }); it(printTitle('guardian', 'cannot add a contract with an existing address'), async () => { const rocketStorage = await RocketStorage.deployed(); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketNewContract', RocketMinipoolManager.abi, rocketStorage.target, { from: guardian, }), 'Guardian added a contract with the same address as an existing one', 'Contract address is already in use'); }); it(printTitle('guardian', 'cannot add a new contract with an invalid name'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addContract', '', RocketMinipoolManager.abi, rocketMinipoolManagerNew.target, { from: guardian, }), 'Added a new contract with an invalid name', 'Invalid contract name'); }); it(printTitle('guardian', 'cannot add a new contract with an empty ABI'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketNewContract', '', rocketMinipoolManagerNew.target, { from: guardian, }), 'Added a new contract with an empty ABI', 'Empty ABI is invalid'); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal to upgrade a network contract, it passees and is executed'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); await helpers.time.increase(60); // Load contracts const rocketStorage = await RocketStorage.deployed(); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalUpgrade', ['upgradeContract', 'rocketNodeManager', compressABI(RocketMinipoolManager.abi), rocketMinipoolManagerNew.target]); // Add the proposal let proposalID = await daoNodeTrustedPropose('hey guys, we really should upgrade this contracts - here\'s a link to its audit reports https://link.com/audit', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); // Proposal has passed, lets execute it now and upgrade the contract await daoNodeTrustedExecute(proposalID, { from: registeredNode1 }); // Check the upgrade proposal is now pending const rocketDAONodeTrustedUpgrade = await RocketDAONodeTrustedUpgrade.deployed(); const count = await rocketDAONodeTrustedUpgrade.getTotal(); assertBN.equal(count, 1n); // Fetch upgrade proposal details const type = await rocketDAONodeTrustedUpgrade.getType(1n); const name = await rocketDAONodeTrustedUpgrade.getName(1n); const address = await rocketDAONodeTrustedUpgrade.getUpgradeAddress(1n); const expectedType = ethers.solidityPackedKeccak256(['string'], ['upgradeContract']); assert.equal(address, rocketMinipoolManagerNew.target); assert.equal(type, expectedType); assert.equal(name, 'rocketNodeManager'); // Upgrade should fail before delay await shouldRevert( rocketDAONodeTrustedUpgrade.connect(registeredNodeTrusted1).execute(1n), 'Was able to upgrade immediately', 'Proposal has not succeeded or has been vetoed or executed'); // Wait for the upgrade delay await helpers.time.increase(upgradeDelay + 1n); // Upgrade should fail from non oDAO member await shouldRevert( rocketDAONodeTrustedUpgrade.connect(userOne).execute(1n), 'Was able to upgrade with non trusted member', 'Invalid trusted node'); // Execute the upgrade await rocketDAONodeTrustedUpgrade.connect(registeredNodeTrusted1).execute(1n); // Check upgrade worked assert.equal(await rocketStorage['getAddress(bytes32)'](ethers.solidityPackedKeccak256(['string', 'string'], ['contract.address', 'rocketNodeManager'])), rocketMinipoolManagerNew.target, 'Contract address was not successfully upgraded'); assert.equal(await rocketStorage.getBool(ethers.solidityPackedKeccak256(['string', 'address'], ['contract.exists', rocketMinipoolManagerNew.target])), true, 'Contract address was not successfully upgraded'); }); it(printTitle('registeredNodeTrusted1', 'creates a proposal for registeredNode1 to join as a new member, member joins, is kicked, then cannot rejoin'), async () => { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Get the DAO settings let daoNodesettings = await RocketDAONodeTrustedSettingsMembers.deployed(); // How much RPL is required for a trusted node bond? let rplBondAmount = await daoNodesettings.getRPLBond(); // Add our 3rd member so proposals can pass await bootstrapMemberAdd(registeredNodeTrusted3, 'rocketpool_3', 'node3@home.com'); // New Member // Encode the calldata for the proposal let proposalCalldata1 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalInvite', ['SaaS_Provider1', 'test@sass.com', registeredNode1.address]); // Add the proposal let proposalId1 = await daoNodeTrustedPropose('hey guys, can we add this cool SaaS member please?', proposalCalldata1, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalId1) - timeCurrent) + 2); // Now lets vote for the new members await daoNodeTrustedVote(proposalId1, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalId1, true, { from: registeredNodeTrusted2 }); // Current time timeCurrent = await helpers.time.latest(); // Fast forward to voting periods finishing await helpers.time.increase((await getDAOProposalEndTime(proposalId1) - timeCurrent) + 2); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalId1, { from: registeredNodeTrusted1 }); // Member has now been invited to join, so lets do that // We'll allow the DAO to transfer our RPL bond before joining await rplMint(registeredNode1, rplBondAmount); await rplAllowanceDAO(registeredNode1, rplBondAmount); // Join now await daoNodeTrustedMemberJoin({ from: registeredNode1 }); // Add a small wait await helpers.time.increase(2); // Check the member is now valid assert.equal(await getDAOMemberIsValid(registeredNode1), true, 'registeredNode1 is not a membmer of the DAO'); // Now we kick the member let proposalCalldata2 = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalKick', [registeredNode1.address, 0]); // Add the proposal let proposalId2 = await daoNodeTrustedPropose('hey guys, this member hasn\'t logged on for weeks, lets boot them with a 33% fine!', proposalCalldata2, { from: registeredNodeTrusted1, }); // Current time timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalId2) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalId2, true, { from: registeredNodeTrusted1 }); await daoNodeTrustedVote(proposalId2, true, { from: registeredNodeTrusted2 }); await daoNodeTrustedVote(proposalId2, true, { from: registeredNodeTrusted3 }); // Proposal has passed, lets execute it now await daoNodeTrustedExecute(proposalId2, { from: registeredNodeTrusted1 }); // The new member has now been kicked assert.equal(await getDAOMemberIsValid(registeredNode1), false, 'registeredNode1 is still a member of the DAO'); // They should not be able to rejoin await rplAllowanceDAO(registeredNode1, rplBondAmount); await shouldRevert(daoNodeTrustedMemberJoin({ from: registeredNode1 }), 'Member was able to join after being kicked', 'This node has not been invited to join'); }); // ABIs - contract address field is ignored it(printTitle('guardian', 'can upgrade a contract ABI in bootstrap mode'), async () => { await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }); }); it(printTitle('guardian', 'cannot upgrade a contract ABI to an identical one in bootstrap mode'), async () => { await setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }); await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Upgraded a contract ABI to an identical one', 'ABIs are identical'); }); it(printTitle('guardian', 'cannot upgrade a contract ABI which does not exist'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'fooBarBaz', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Upgraded a contract ABI which did not exist', 'ABI does not exist'); }); it(printTitle('userOne', 'cannot upgrade a contract ABI'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('upgradeABI', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: userOne, }), 'Random address upgraded a contract ABI', 'Account is not a temporary guardian'); }); it(printTitle('guardian', 'can not set "reduced.bond" to a value not divisible by milliwei'), async () => { // Can set to 2 ether + 1 milliwei await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', 2001000000000000000n, { from: guardian }); // Cannot set to 1 ether + 1 milliwei + 1 microwei await shouldRevert( setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', 1001001000000000000n, { from: guardian }), 'Was able to set "reduced.bond" to value not divisible by milliwei', 'Value must be divisible by milliwei', ); }); it(printTitle('guardian', 'can not set "megapool.dissolve.penalty" to zero'), async () => { await shouldRevert( setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', 0n, { from: guardian }), 'Was able to set "megapool.dissolve.penalty" to zero', 'Value must be >= 0.01 ETH', ); }); it(printTitle('guardian', 'can add a contract ABI in bootstrap mode'), async () => { await setDaoNodeTrustedBootstrapUpgrade('addABI', 'rocketNewFeature', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }); }); it(printTitle('guardian', 'cannot add a new contract ABI with an invalid name'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addABI', '', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Added a new contract ABI with an invalid name', 'Invalid ABI name'); }); it(printTitle('guardian', 'cannot add a new contract ABI with an empty ABI'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addABI', 'rocketNewFeatures', '', '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Added a new contract ABI with an empty ABI', 'Empty ABI is invalid'); }); it(printTitle('guardian', 'cannot add a new contract ABI with an existing name'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addABI', 'rocketNodeManager', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: guardian, }), 'Added a new contract ABI with an existing name', 'ABI name is already in use'); }); it(printTitle('userOne', 'cannot add a new contract ABI'), async () => { await shouldRevert(setDaoNodeTrustedBootstrapUpgrade('addABI', 'rocketNewFeature', RocketMinipoolManager.abi, '0x0000000000000000000000000000000000000000', { from: userOne, }), 'Random address added a new contract ABI', 'Account is not a temporary guardian'); }); }); } ================================================ FILE: test/dao/dao-protocol-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { setDAOProtocolBootstrapEnableGovernance, setDaoProtocolBootstrapModeDisabled, setDAOProtocolBootstrapSecurityInvite, setDAOProtocolBootstrapSetting, setDAOProtocolBootstrapSettingAddressList, setDAOProtocolBootstrapSettingMulti, } from './scenario-dao-protocol-bootstrap'; import { RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, RocketDAOProtocolSettingsMegapool, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsProposals, RocketDAOProtocolSettingsRewards, } from '../_utils/artifacts'; import { cloneLeaves, constructTreeLeaves, daoProtocolClaimBondChallenger, daoProtocolClaimBondProposer, daoProtocolCreateChallenge, daoProtocolDefeatProposal, daoProtocolExecute, daoProtocolFinalise, daoProtocolGenerateChallengeProof, daoProtocolGeneratePollard, daoProtocolGenerateVoteProof, daoProtocolOverrideVote, daoProtocolPropose, daoProtocolSubmitRoot, daoProtocolVote, getDelegatedVotingPower, getPhase2VotingPower, getSubIndex, setDaoProtocolNodeCommissionShare, setDaoProtocolNodeShareSecurityCouncilAdder, setDaoProtocolVoterShare, } from './scenario-dao-protocol'; import { getNodeCount, nodeSetDelegate, nodeStakeRPL, registerNode, setRPLLockingAllowed } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { userDeposit } from '../_helpers/deposit'; import { getDaoProtocolChallengeBond, getDaoProtocolChallengePeriod, getDaoProtocolDepthPerRound, getDaoProtocolProposalBond, getDaoProtocolVoteDelayTime, getDaoProtocolVotePhase1Time, getDaoProtocolVotePhase2Time, } from '../_helpers/dao'; import { assertBN } from '../_helpers/bn'; import { daoSecurityMemberJoin, getDAOSecurityMemberIsValid } from './scenario-dao-security'; import { voteStates } from './scenario-dao-proposal'; import * as assert from 'assert'; import { globalSnapShot } from '../_utils/snapshotting'; import { nodeDepositMulti } from '../_helpers/megapool'; import { unstakeRpl } from '../node/scenario-unstake-rpl'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketDAOProtocol', () => { // Accounts let owner, random, proposer, node1, node2, securityMember1, allowListed; let nodeMap = {}; // Settings to retrieve let depthPerRound; let challengeBond; let proposalBond; let challengePeriod; let voteDelayTime; let votePhase1Time; let votePhase2Time; // Settings to apply const secondsPerEpoch = 384; const rewardClaimBalanceIntervals = 28; const balanceSubmissionFrequency = (24 * 60 * 60); const rewardClaimPeriodTime = (rewardClaimBalanceIntervals * balanceSubmissionFrequency * secondsPerEpoch); // 28 days // Setup before(async () => { await globalSnapShot(); [ owner, random, proposer, node1, node2, securityMember1, allowListed, ] = await ethers.getSigners(); // Add some ETH into the DP await userDeposit({ from: random, value: '320'.ether }); // Retrieve settings depthPerRound = await getDaoProtocolDepthPerRound(); challengeBond = await getDaoProtocolChallengeBond(); proposalBond = await getDaoProtocolProposalBond(); challengePeriod = await getDaoProtocolChallengePeriod(); voteDelayTime = await getDaoProtocolVoteDelayTime(); votePhase1Time = await getDaoProtocolVotePhase1Time(); votePhase2Time = await getDaoProtocolVotePhase2Time(); // Set the reward claim period await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.balances.frequency', balanceSubmissionFrequency, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsRewards, 'rewards.claimsperiods', rewardClaimBalanceIntervals, { from: owner }); // Set maximum minipool count higher for test await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.maximum.count', 100, { from: owner }); }); // // Utilities // function burnAmount(bond) { return bond * '0.2'.ether / '1'.ether; } function bondAfterBurn(bond) { return bond - burnAmount(bond); } // // Start Tests // // Update a setting it(printTitle('random', 'fails to update a setting as they are not the guardian'), async () => { // Fails to change a setting await shouldRevert(setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.create.enabled', true, { from: random, }), 'User updated bootstrap setting', 'Account is not a temporary guardian'); }); // Update multiple settings it(printTitle('random', 'fails to update multiple settings as they are not the guardian'), async () => { // Fails to change multiple settings await shouldRevert(setDAOProtocolBootstrapSettingMulti([ RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, ], [ 'auction.lot.create.enabled', 'deposit.minimum', 'rpl.inflation.interval.blocks', ], [ true, '2'.ether, 400, ], { from: random, }), 'User updated bootstrap setting', 'Account is not a temporary guardian'); }); // Verify each setting contract is enabled correctly. These settings are tested in greater detail in the relevent contracts it(printTitle('guardian', 'updates a setting in each settings contract while bootstrap mode is enabled'), async () => { // Set via bootstrapping await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.create.enabled', true, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.minimum', '2'.ether, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsInflation, 'rpl.inflation.interval.blocks', 400, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.submit.withdrawable.enabled', true, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.prices.enabled', true, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsRewards, 'rpl.rewards.claim.period.blocks', 100, { from: owner, }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsInflation, 'network.reth.deposit.delay', 500, { from: owner, }); }); it(printTitle('guardian', 'cannot update "user.distribute.delay.shortfall" lower than "user.distribute.delay"'), async () => { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'user.distribute.delay.shortfall', 10000, { from: owner, }) await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'user.distribute.delay', 8000, { from: owner, }); // Cannot set delay with shortfall lower await shouldRevert( setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'user.distribute.delay.shortfall', 7000, { from: owner, }), 'Was able to set delay with shortfall lower than regular delay', 'Value must be >= user.distribute.delay'); // Cannot set regular delay higher await shouldRevert( setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'user.distribute.delay', 11000, { from: owner, }), 'Was able to set delay higher than delay with shortfall', 'Value must be <= user.distribute.delay.shortfall'); }); // Verify each setting contract is enabled correctly. These settings are tested in greater detail in the relevent contracts it(printTitle('guardian', 'updates multiple settings at once while bootstrap mode is enabled'), async () => { // Set via bootstrapping await setDAOProtocolBootstrapSettingMulti([ RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, ], [ 'auction.lot.create.enabled', 'deposit.minimum', 'rpl.inflation.interval.blocks', ], [ true, '2'.ether, 400, ], { from: owner, }); }); // Update a setting, then try again it(printTitle('guardian', 'updates a setting, then fails to update a setting again after bootstrap mode is disabled'), async () => { // Set via bootstrapping await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.create.enabled', true, { from: owner, }); // Enable governance so we can disable bootstrap await setDAOProtocolBootstrapEnableGovernance({ from: owner }); // Disable bootstrap mode await setDaoProtocolBootstrapModeDisabled({ from: owner, }); // Attempt to change a setting again await shouldRevert(setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsAuction, 'auction.lot.create.enabled', true, { from: owner, }), 'Guardian updated bootstrap setting after mode disabled', 'Bootstrap mode not engaged'); }); // Update multiple settings, then try again it(printTitle('guardian', 'updates multiple settings, then fails to update multiple settings again after bootstrap mode is disabled'), async () => { // Set via bootstrapping await setDAOProtocolBootstrapSettingMulti([ RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, ], [ 'auction.lot.create.enabled', 'deposit.minimum', 'rpl.inflation.interval.blocks', ], [ true, '2'.ether, 400, ], { from: owner, }); // Enable governance so we can disable bootstrap await setDAOProtocolBootstrapEnableGovernance({ from: owner }); // Disable bootstrap mode await setDaoProtocolBootstrapModeDisabled({ from: owner, }); // Attempt to change a setting again await shouldRevert(setDAOProtocolBootstrapSettingMulti([ RocketDAOProtocolSettingsAuction, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsInflation, ], [ 'auction.lot.create.enabled', 'deposit.minimum', 'rpl.inflation.interval.blocks', ], [ true, '2'.ether, 400, ], { from: owner, }), 'Guardian updated bootstrap setting after mode disabled', 'Bootstrap mode not engaged'); }); async function createNode(validatorCount, node) { // Stake RPL for voting power let rplStake = '100'.ether * validatorCount.BN; const nodeCount = await getNodeCount(); await registerNode({ from: node }); nodeMap[node.address] = Number(nodeCount); await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); // Create validators const deposits = Array(validatorCount).fill({ bondAmount: '4'.ether, useExpressTicket: false, }); await nodeDepositMulti(node, deposits); // Allow RPL locking by default await setRPLLockingAllowed(node, true, { from: node }); } async function createValidProposal(name = 'Test proposal', payload = '0x00', block = null) { // Setup if (block === null) { block = await ethers.provider.getBlockNumber(); } const power = await getDelegatedVotingPower(block); const leaves = constructTreeLeaves(power); // Create the proposal let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound); let propId = await daoProtocolPropose(name, payload, block, pollard, { from: proposer }); return { block, propId, power, leaves, }; } async function mockNodeSet() { let accounts = await ethers.getSigners(); let newAccounts = []; for (let i = 10; i < 50; i++) { // Create pseudo-random number of minpools const count = ((i * 7) % 5) + 1; await createNode(count, accounts[i]); newAccounts.push(accounts[i]); } return newAccounts; } async function voteAll(proposalId, leaves, direction) { let accounts = await ethers.getSigners(); // Vote from each account until the proposal passes for (let i = 10; i < 50; i++) { const nodeIndex = nodeMap[accounts[i].address]; const voteProof = daoProtocolGenerateVoteProof(leaves, nodeIndex); try { if (voteProof.sum > 0n) { await daoProtocolVote(proposalId, direction, voteProof.sum, nodeIndex, voteProof.witness, { from: accounts[i] }); } } catch (e) { if (e.message.indexOf('Proposal has passed') !== -1) { return; } else { throw e; } } } } function getRoundCount(leafCount) { const maxDepth = Math.ceil(Math.log2(leafCount)); const totalLeaves = 2 ** maxDepth; let rounds = Math.ceil(Math.floor(Math.log2(totalLeaves)) / depthPerRound) - 1; if (rounds === 0) { rounds = 1; } return rounds; } function getMaxDepth(leafCount) { return Math.ceil(Math.log2(leafCount)); } function getChallengeIndices(finalIndex, leafCount) { const phase1Indices = []; const phase2Indices = []; const phase1Depth = getMaxDepth(leafCount); const phase2Depth = phase1Depth * 2; const subRootIndex = finalIndex / Math.pow(2, phase1Depth); const roundsPerPhase = getRoundCount(leafCount); for (let i = 1; i <= roundsPerPhase; i++) { let challengeDepth = i * depthPerRound; if (challengeDepth <= phase1Depth) { const index = subRootIndex / (2 ** (phase1Depth - challengeDepth)); if (index !== subRootIndex) { phase1Indices.push(index); } } } for (let i = 1; i <= roundsPerPhase; i++) { let challengeDepth = phase1Depth + (i * depthPerRound); if (challengeDepth <= phase2Depth) { phase2Indices.push(finalIndex / (2 ** (phase2Depth - challengeDepth))); } } return { phase1Indices, subRootIndex, phase2Indices }; } /* * Proposer */ describe('With Node Operators', () => { let nodes; before(async () => { nodes = await mockNodeSet(); await createNode(1, proposer); }); it(printTitle('proposer', 'can not create a proposal until enabled by guardian'), async () => { // Create a valid proposal await shouldRevert(createValidProposal(), 'Was able to create proposal', 'DAO has not been enabled'); }); it(printTitle('proposer', 'can disable bootstrap after enabling DAO'), async () => { // Should fail before enabling governance await shouldRevert(setDaoProtocolBootstrapModeDisabled({ from: owner }), 'Was able to disable bootstrap', 'On-chain governance must be enabled first'); // Enable governance await setDAOProtocolBootstrapEnableGovernance({ from: owner }); // Should succeed await setDaoProtocolBootstrapModeDisabled({ from: owner }); }); describe('With Governance Enabled', () => { before(async () => { await setDAOProtocolBootstrapEnableGovernance({ from: owner }); }); it(printTitle('proposer', 'can successfully submit a proposal'), async () => { await createValidProposal(); }); it(printTitle('proposer', 'can not submit a proposal with a past block'), async () => { const block = await ethers.provider.getBlockNumber() + 5; await shouldRevert(createValidProposal('Test proposal', '0x00', block), 'Was able to create proposal with future block', 'Block must be in the past'); }); it(printTitle('proposer', 'can not submit a proposal if locking is not allowed'), async () => { // Setup await setRPLLockingAllowed(proposer, false, { from: proposer }); // Create a valid proposal await shouldRevert(createValidProposal(), 'Was able to create proposal', 'Node is not allowed to lock RPL'); }); it(printTitle('proposer', 'can unstake excess RPL after it is unlocked'), async () => { // Give the proposer 150% collateral + proposal bond + 50 await mintRPL(owner, proposer, '2390'.ether); await nodeStakeRPL('2390'.ether, { from: proposer }); // Create a valid proposal const { propId } = await createValidProposal(); // Wait for withdraw cooldown await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond await daoProtocolClaimBondProposer(propId, [1], { from: proposer }); // Wait the withdrawal cooldown time await helpers.time.increase(rewardClaimPeriodTime + 1); // Unstake excess await unstakeRpl('150'.ether, { from: proposer }); }); it(printTitle('proposer', 'can not create proposal with invalid leaf count'), async () => { // Try to create invalid proposal const block = await ethers.provider.getBlockNumber(); const power = await getDelegatedVotingPower(block); const leaves = constructTreeLeaves(power); // Too few let invalidLeaves = leaves.slice(0, 1); await shouldRevert(daoProtocolPropose('Test proposal', '0x00', block, invalidLeaves, { from: proposer }), 'Was able to create proposal', 'Invalid node count'); // Too many invalidLeaves = [...leaves, ...leaves]; await shouldRevert(daoProtocolPropose('Test proposal', '0x00', block, invalidLeaves, { from: proposer }), 'Was able to create proposal', 'Invalid node count'); }); it(printTitle('proposer', 'can not claim bond on defeated proposal'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger }); // Try to claim bond await shouldRevert(daoProtocolClaimBondProposer(propId, [1], { from: proposer }), 'Was able to claim bond', 'Proposal defeated'); }); it(printTitle('proposer', 'can not claim bond twice'), async () => { // Create a minipool with a node to use as a challenger await createNode(1, node1); // Create a valid proposal const { propId } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond await daoProtocolClaimBondProposer(propId, [1], { from: proposer }); // Try claim bond again await shouldRevert(daoProtocolClaimBondProposer(propId, [1], { from: proposer }), 'Claimed bond twice', 'Invalid challenge state'); }); it(printTitle('proposer', 'can not claim reward twice'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Create some invalid challenges const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices.slice(0, 1); for (const index of indices) { // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond and rewards await daoProtocolClaimBondProposer(propId, [1, ...indices], { from: proposer }); // Try claim reward again await shouldRevert(daoProtocolClaimBondProposer(propId, [indices[0]], { from: proposer }), 'Claimed reward twice', 'Invalid challenge state'); }); it(printTitle('proposer', 'can not claim reward for unresponded index'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Create some invalid challenges const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices.slice(0, 1); const index = indices[0]; // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Try to claim reward for unresponded index await shouldRevert(daoProtocolClaimBondProposer(propId, [indices[0]], { from: proposer }), 'Was able to claim reward', 'Invalid challenge state'); }); it(printTitle('proposer', 'can not claim reward for unchallenged index'), async () => { // Create a minipool with a node to use as a challenger await createNode(1, node1); // Create a valid proposal const { propId } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Try to claim reward for unchallenged index await shouldRevert(daoProtocolClaimBondProposer(propId, [2], { from: proposer }), 'Was able to claim reward', 'Invalid challenge state'); }); it(printTitle('proposer', 'can not respond to a challenge with an invalid pollard'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); // Try with an invalid nodes (incorrect node count) await shouldRevert(daoProtocolSubmitRoot(propId, index, pollard.slice(0, 1), { from: proposer }), 'Accepted invalid nodes', 'Invalid node count'); // Try with an invalid nodes (invalid node sum) let invalidNodes = cloneLeaves(pollard); invalidNodes[0].sum = invalidNodes[0].sum + 1n; await shouldRevert(daoProtocolSubmitRoot(propId, index, invalidNodes, { from: proposer }), 'Accepted invalid nodes', 'Invalid sum'); // Try with an invalid nodes (invalid node hash) invalidNodes = cloneLeaves(pollard); invalidNodes[0].hash = '0x'.padEnd(66, '0'); await shouldRevert(daoProtocolSubmitRoot(propId, index, invalidNodes, { from: proposer }), 'Accepted invalid nodes', 'Invalid hash'); }); it(printTitle('proposer', 'can not respond to a challenge with an invalid leaves'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create an invalid proposal const block = await ethers.provider.getBlockNumber(); let power = await getDelegatedVotingPower(block); power[0] = '1000'.ether; const leaves = constructTreeLeaves(power); // Create the proposal let nodes = await daoProtocolGeneratePollard(leaves, depthPerRound); let propId = await daoProtocolPropose('Test proposal', '0x00', block, nodes, { from: proposer }); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); // Phase 1 for (const index of phase1Indices) { // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, subRootIndex); await daoProtocolCreateChallenge(propId, subRootIndex, challenge.node, challenge.proof, { from: challenger }); // Generate the sub tree const challengedNodeId = subRootIndex - (2 ** phase1Depth); let subTreePower = await getPhase2VotingPower(block, challengedNodeId); subTreePower[0] = '1000'.ether; const subTreeLeaves = await constructTreeLeaves(subTreePower); let subIndex = getSubIndex(subRootIndex, subTreeLeaves); let pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await daoProtocolSubmitRoot(propId, subRootIndex, pollard, { from: proposer }); // Phase 2 for (const index of phase2Indices.slice(0, phase2Indices.length - 1)) { // Challenge let subIndex = getSubIndex(index, subTreeLeaves); const challenge = daoProtocolGenerateChallengeProof(subTreeLeaves, depthPerRound, subIndex); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } const finalChallengeIndex = phase2Indices[phase2Indices.length - 1]; // Challenge final round // await daoProtocolCreateChallenge(propId, finalChallengeIndex, { from: challenger }); subIndex = getSubIndex(finalChallengeIndex, subTreeLeaves); challenge = daoProtocolGenerateChallengeProof(subTreeLeaves, depthPerRound, subIndex); await daoProtocolCreateChallenge(propId, finalChallengeIndex, challenge.node, challenge.proof, { from: challenger }); // Response pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await shouldRevert(daoProtocolSubmitRoot(propId, finalChallengeIndex, pollard, { from: proposer }), 'Accepted invalid leaves', 'Invalid leaves'); }); it(printTitle('proposer', 'can not respond to a challenge with an invalid leaves (invalid primary tree leaf hash)'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create an invalid proposal const block = await ethers.provider.getBlockNumber(); let power = await getDelegatedVotingPower(block); let leaves = constructTreeLeaves(power); leaves[0].sum = leaves[0].sum + 100000n; // Create the proposal let nodes = await daoProtocolGeneratePollard(leaves, depthPerRound); let propId = await daoProtocolPropose('Test proposal', '0x00', block, nodes, { from: proposer }); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); // Phase 1 for (const index of phase1Indices) { // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, subRootIndex); await daoProtocolCreateChallenge(propId, subRootIndex, challenge.node, challenge.proof, { from: challenger }); // Generate the subtree const challengedNodeId = subRootIndex - (2 ** phase1Depth); const subTreePower = await getPhase2VotingPower(block, challengedNodeId); let subTreeLeaves = await constructTreeLeaves(subTreePower); subTreeLeaves[0].sum = subTreeLeaves[0].sum + 100000n; let subIndex = getSubIndex(subRootIndex, subTreeLeaves); let pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await shouldRevert(daoProtocolSubmitRoot(propId, subRootIndex, pollard, { from: proposer }), 'Accepted invalid hash', 'Invalid hash'); }); it(printTitle('voter', 'can not set delegate to same value'), async () => { await nodeSetDelegate(nodes[1].address, { from: nodes[0] }); await shouldRevert( nodeSetDelegate(nodes[1].address, { from: nodes[0] }), 'Was able to set delegate to same value', 'Delegate already set to value', ); }); /** * Override Votes */ it(printTitle('voter', 'can vote against their delegate'), async () => { // Setup await nodeSetDelegate(nodes[1].address, { from: nodes[0] }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip phase 1 of the voting period await helpers.time.increase(votePhase1Time + 1); // Have node[0] vote against vote[1]s for vote with an against vote await daoProtocolOverrideVote(propId, voteStates.Against, { from: nodes[0] }); }); it(printTitle('voter', 'can not override vote in the same direction as their delegate'), async () => { // Setup await nodeSetDelegate(nodes[1].address, { from: nodes[0] }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip phase 1 of the voting period await helpers.time.increase(votePhase1Time + 1); // Try to override vote with a for (checks for failure internally) await daoProtocolOverrideVote(propId, voteStates.For, { from: nodes[0] }); }); it(printTitle('voter', 'can vote in same direction as delegate if both voting in phase 2'), async () => { // Setup await nodeSetDelegate(nodes[1].address, { from: nodes[0] }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Skip phase 1 of the voting period await helpers.time.increase(votePhase1Time + 1); // Vote for from delegate await daoProtocolOverrideVote(propId, voteStates.For, { from: nodes[1] }); // Vote for from node await daoProtocolOverrideVote(propId, voteStates.For, { from: nodes[0] }); }); it(printTitle('voter', 'can not vote in phase 1 then in phase 2'), async () => { // Setup await nodeSetDelegate(nodes[1].address, { from: nodes[0] }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote as a delegate const nodeIndex = nodeMap[nodes[1].address]; const voteProof = daoProtocolGenerateVoteProof(leaves, nodeIndex); await daoProtocolVote(propId, voteStates.For, voteProof.sum, nodeIndex, voteProof.witness, { from: nodes[1] }); // Skip phase 1 of the voting period await helpers.time.increase(votePhase1Time + 1); // Try to override own vote await shouldRevert(daoProtocolOverrideVote(propId, voteStates.Against, { from: nodes[1] }), 'Was able to override self', 'Node operator has already voted on proposal'); }); /** * Failed Proposals */ it(printTitle('proposer', 'cannot execute a failed proposal'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Invite security council member let ABI = ['function proposalSecurityInvite(string,address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityInvite', ['Security Member 1', securityMember1.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Invite security member to the council', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.Against); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Fail to execute the proposal await shouldRevert(daoProtocolExecute(propId, { from: proposer }), 'Was able to execute failed proposal', 'Proposal has not succeeded, has expired or has already been executed'); // Fail to accept the invitation await shouldRevert(daoSecurityMemberJoin({ from: securityMember1 }), 'Was able to join on failed invite', 'This address has not been invited to join'); }); it(printTitle('proposer', 'can not execute a vetoed proposal but can destroy it'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Invite security council member let ABI = ['function proposalSecurityInvite(string,address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityInvite', ['Security Member 1', securityMember1.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Invite security member to the council', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.AgainstWithVeto); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Fail to execute the proposal await shouldRevert(daoProtocolExecute(propId, { from: proposer }), 'Was able to execute failed proposal', 'Proposal has not succeeded, has expired or has already been executed'); // Finalise the vetoed proposal await daoProtocolFinalise(propId, { from: proposer }); }); /** * Successful Proposals */ it(printTitle('proposer', 'can invite a security council member'), async () => { // Invite security council member let ABI = ['function proposalSecurityInvite(string,address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityInvite', ['Security Member 1', securityMember1.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Invite security member to the council', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Execute the proposal await daoProtocolExecute(propId, { from: proposer }); // Accept the invitation await daoSecurityMemberJoin({ from: securityMember1 }); }); it(printTitle('proposer', 'can kick a security council member'), async () => { // Setup await setDAOProtocolBootstrapSecurityInvite('Member', securityMember1, { from: owner }); await daoSecurityMemberJoin({ from: securityMember1 }); // Invite security council member let ABI = ['function proposalSecurityKick(address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityKick', [securityMember1.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Kick security member from the council', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Execute the proposal await daoProtocolExecute(propId, { from: proposer }); // Member should no longer exists assert.equal(await getDAOSecurityMemberIsValid(securityMember1), false, 'Member still exists in council'); }); it(printTitle('proposer', 'can not kick a security council member that does not exist'), async () => { // Setup await setDAOProtocolBootstrapSecurityInvite('Member', securityMember1, { from: owner }); await daoSecurityMemberJoin({ from: securityMember1 }); // Invite security council member let ABI = ['function proposalSecurityKick(address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityKick', [random.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Kick security member from the council', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Execute the proposal await shouldRevert( daoProtocolExecute(propId, { from: proposer }), 'Was able to kick non-existing member', 'This node is not part of the security council', ); }); it(printTitle('proposer', 'can replace a security council member'), async () => { // Setup await setDAOProtocolBootstrapSecurityInvite('Member', securityMember1, { from: owner }); await daoSecurityMemberJoin({ from: securityMember1 }); // Invite security council member let ABI = ['function proposalSecurityReplace(address, string, address)']; let iface = new ethers.Interface(ABI); let proposalCalldata = iface.encodeFunctionData('proposalSecurityReplace', [securityMember1.address, 'Replaced Member 1', random.address]); // Create a valid proposal const { propId, leaves, } = await createValidProposal('Replace security council member', proposalCalldata); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Vote all in favour await voteAll(propId, leaves, voteStates.For); // Skip the full vote period await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Execute the proposal await daoProtocolExecute(propId, { from: proposer }); // Accept on new member address await daoSecurityMemberJoin({ from: random }); // Old member should no longer exists assert.equal(await getDAOSecurityMemberIsValid(securityMember1), false, 'Member still exists in council'); // New member should exit assert.equal(await getDAOSecurityMemberIsValid(random), true, 'Member is not in council'); }); /** * Challenger */ it(printTitle('challenger', 'can not challenge with insufficient RPL'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Set challenge bond to some high value await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsProposals, 'proposal.challenge.bond', '10000'.ether, { from: owner }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge', 'Not enough staked RPL'); }); it(printTitle('challenger', 'can not challenge if locking RPL is not allowed'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); await setRPLLockingAllowed(challenger, false, { from: challenger }); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge', 'Node is not allowed to lock RPL'); }); it(printTitle('challenger', 'can claim share on defeated proposal'), async () => { // Create a minipool with a node to use as challengers let challenger1 = node1; await createNode(1, challenger1); let challenger2 = node2; await createNode(1, challenger2); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const { phase1Indices, subRootIndex } = getChallengeIndices(2 ** maxDepth, leaves.length); const indices = [...phase1Indices, subRootIndex]; // Challenge first round let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, indices[0]); await daoProtocolCreateChallenge(propId, indices[0], challenge.node, challenge.proof, { from: challenger1 }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, indices[0]); await daoProtocolSubmitRoot(propId, indices[0], pollard, { from: proposer }); // Challenge second round challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, indices[1]); await daoProtocolCreateChallenge(propId, indices[1], challenge.node, challenge.proof, { from: challenger2 }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, indices[1], { from: challenger2 }); // Claim bond on invalid index const deltas1 = await daoProtocolClaimBondChallenger(propId, [indices[0]], { from: challenger1 }); const deltas2 = await daoProtocolClaimBondChallenger(propId, [indices[1]], { from: challenger2 }); // Each should receive 1/2 of the proposal bond as a reward and their challenge bond back (with 20% being burned) assertBN.equal(deltas1.staked, bondAfterBurn(proposalBond / 2n)); assertBN.equal(deltas2.staked, bondAfterBurn(proposalBond / 2n)); assertBN.equal(deltas1.locked, -challengeBond); assertBN.equal(deltas2.locked, -challengeBond); assertBN.equal(deltas1.burned, burnAmount(proposalBond / 2n)); assertBN.equal(deltas2.burned, burnAmount(proposalBond / 2n)); }); it(printTitle('challenger', 'can recover bond if index was not used'), async () => { // Create a minipool with a node to use as a challenger let challenger1 = node1; await createNode(1, challenger1); let challenger2 = node2; await createNode(1, challenger2); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger1 }); challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index + 1); await daoProtocolCreateChallenge(propId, index + 1, challenge.node, challenge.proof, { from: challenger2 }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger1 }); // Recover bond const deltas1 = await daoProtocolClaimBondChallenger(propId, [index], { from: challenger1 }); const deltas2 = await daoProtocolClaimBondChallenger(propId, [index + 1], { from: challenger2 }); assertBN.equal(deltas1.locked, -challengeBond); assertBN.equal(deltas1.staked, bondAfterBurn(proposalBond)); assertBN.equal(deltas1.burned, burnAmount(proposalBond)); assertBN.equal(deltas2.locked, -challengeBond); assertBN.equal(deltas2.staked, 0n); assertBN.equal(deltas2.burned, 0n); }); /** * Other */ it(printTitle('other', 'can not claim reward on challenge they did not make'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create node for invalid claim await createNode(1, node2); // Create a valid proposal const { propId, leaves } = await createValidProposal(); // Challenge/response const phase1Depth = getMaxDepth(leaves.length); const maxDepth = phase1Depth * 2; const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger }); // Claim bond on invalid index await shouldRevert(daoProtocolClaimBondChallenger(propId, [indices[0]], { from: node2 }), 'Was able to claim reward', 'Invalid challenger'); }); it(printTitle('other', 'can not claim bond on a proposal they did not make'), async () => { // Create a minipool with a node to use as a challenger let challenger = node1; await createNode(1, challenger); // Create node for invalid claim await createNode(1, node2); // Create a valid proposal const { propId } = await createValidProposal(); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond on invalid index await shouldRevert(daoProtocolClaimBondProposer(propId, [1], { from: node2 }), 'Was able to claim proposal bond', 'Not proposer'); }); describe('With Valid Proposal', () => { let challenger; let propId, leaves, block; let phase1Depth, maxDepth; before(async () => { // Create a minipool with a node to use as a challenger challenger = node1; await createNode(1, challenger); // Create a valid proposal let proposal = await createValidProposal(); propId = proposal.propId; leaves = proposal.leaves; block = proposal.block; phase1Depth = getMaxDepth(leaves.length); maxDepth = phase1Depth * 2; }); it(printTitle('proposer', 'can successfully refute an invalid challenge'), async () => { // Challenge/response const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); // Phase 1 for (const index of phase1Indices) { // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, subRootIndex); await daoProtocolCreateChallenge(propId, subRootIndex, challenge.node, challenge.proof, { from: challenger }); // Generate the subtree const challengedNodeId = subRootIndex - (2 ** phase1Depth); const subTreePower = await getPhase2VotingPower(block, challengedNodeId); const subTreeLeaves = await constructTreeLeaves(subTreePower); let subIndex = getSubIndex(subRootIndex, subTreeLeaves); let pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await daoProtocolSubmitRoot(propId, subRootIndex, pollard, { from: proposer }); // Phase 2 for (const index of phase2Indices) { // Challenge let subIndex = getSubIndex(index, subTreeLeaves); const challenge = daoProtocolGenerateChallengeProof(subTreeLeaves, depthPerRound, subIndex); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(subTreeLeaves, depthPerRound, subIndex); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } }); it(printTitle('proposer', 'can not respond to a challenge after proposal enters voting'), async () => { // Challenge/response const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); // Challenge let index = phase1Indices[0]; const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let proposal enter voting await helpers.time.increase(voteDelayTime + 1); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await shouldRevert(daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }), 'Was able to submit root', 'Can not submit root for a valid proposal'); }); it(printTitle('proposer', 'can successfully claim proposal bond'), async () => { // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond const deltas = await daoProtocolClaimBondProposer(propId, [1], { from: proposer }); assertBN.equal(deltas.locked, -proposalBond); assertBN.equal(deltas.staked, 0n); assertBN.equal(deltas.burned, 0n); }); it(printTitle('proposer', 'can successfully claim invalid challenge'), async () => { // Create some challenges const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices.slice(0, 1); for (const index of indices) { // Challenge const challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); } // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond and rewards const deltas = await daoProtocolClaimBondProposer(propId, [1, ...indices], { from: proposer }); assertBN.equal(deltas.locked, -proposalBond); assertBN.equal(deltas.staked, bondAfterBurn(challengeBond) * (indices.length.toString().BN)); assertBN.equal(deltas.burned, burnAmount(challengeBond) * (indices.length.toString().BN)); }); it(printTitle('proposer', 'can not create proposal without enough RPL stake'), async () => { // Not enough bond to create a second await shouldRevert(createValidProposal(), 'Was able to create proposal', 'Not enough staked RPL'); }); it(printTitle('challenger', 'can not create challenge with proof from a deeper index'), async () => { const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); const index = phase1Indices[0]; const proofIndex = phase1Indices[1]; // Create challenge using lower index let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, proofIndex); // Proof length should be invalid await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Challenge was submitted', 'Invalid proof length'); }); it(printTitle('challenger', 'can recover bond if proposal was successful'), async () => { // Challenge/response const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Wait for proposal wait period to end await helpers.time.increase(voteDelayTime + 1); // Let the proposal expire to unlock the bond await helpers.time.increase(votePhase1Time + votePhase2Time + 1); // Claim bond on invalid index const deltas = await daoProtocolClaimBondChallenger(propId, [index], { from: challenger }); assertBN.equal(deltas.locked, -challengeBond); assertBN.equal(deltas.staked, 0n); assertBN.equal(deltas.burned, 0n); }); it(printTitle('challenger', 'can not claim bond on index twice'), async () => { // Challenge/response const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger }); // Claim bond on invalid index await daoProtocolClaimBondChallenger(propId, [indices[0]], { from: challenger }); // Try claim again await shouldRevert(daoProtocolClaimBondChallenger(propId, [indices[0]], { from: challenger }), 'Claimed twice', 'Invalid challenge state'); }); it(printTitle('challenger', 'can not challenge an index with greater depth than max'), async () => { // Challenge/response const badIndex = 2 ** (maxDepth + 1); // Challenge await shouldRevert(daoProtocolCreateChallenge(propId, badIndex, leaves[0], [], { from: challenger }), 'Was able to challenge invalid index', 'Invalid index depth'); }); it(printTitle('challenger', 'can not defeat a proposal before challenge period passes'), async () => { // Challenge/response const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Defeat it await shouldRevert(daoProtocolDefeatProposal(propId, index, { from: challenger }), 'Was able to claim before period', 'Not enough time has passed'); }); it(printTitle('challenger', 'can not defeat a proposal once in Active state'), async () => { // Challenge/response const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge pass await helpers.time.increase(voteDelayTime + 1); // Defeat it await shouldRevert(daoProtocolDefeatProposal(propId, index, { from: challenger }), 'Was able to defeat successful proposal', 'Can not defeat a valid proposal'); }); it(printTitle('challenger', 'can not claim bond while proposal is pending'), async () => { // Challenge/response const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); const index = phase1Indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Try to claim challenge bond await shouldRevert(daoProtocolClaimBondChallenger(propId, [index], { from: challenger }), 'Claimed while pending', 'Can not claim bond while proposal is Pending'); }); it(printTitle('challenger', 'can not challenge the same index twice'), async () => { // Challenge/response const indices = getChallengeIndices(2 ** maxDepth, leaves.length).phase1Indices; const index = indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge an index twice', 'Index already challenged'); }); it(printTitle('challenger', 'can not challenge an index with an unchallenged parent'), async () => { // Challenge/response const index = getChallengeIndices(2 ** maxDepth, leaves.length).subRootIndex; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge invalid index', 'Invalid challenge depth'); }); it(printTitle('challenger', 'can not challenge a defeated proposal'), async () => { const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); const index = phase1Indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger }); // Try challenge the next node challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index + 1); await shouldRevert(daoProtocolCreateChallenge(propId, index + 1, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge', 'Can only challenge while proposal is Pending'); }); it(printTitle('challenger', 'can not challenge after pending state'), async () => { const { phase1Indices, subRootIndex, phase2Indices, } = getChallengeIndices(2 ** maxDepth, leaves.length); const index = phase1Indices[0]; // Let the challenge expire await helpers.time.increase(voteDelayTime + 1); // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await shouldRevert(daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }), 'Was able to challenge', 'Can only challenge while proposal is Pending'); }); it(printTitle('challenger', 'can not claim bond on invalid index'), async () => { // Challenge/response const challengeIndices = getChallengeIndices(2 ** maxDepth, leaves.length); const index = challengeIndices.phase1Indices[0]; // Challenge let challenge = daoProtocolGenerateChallengeProof(leaves, depthPerRound, index); await daoProtocolCreateChallenge(propId, index, challenge.node, challenge.proof, { from: challenger }); // Let the challenge expire await helpers.time.increase(challengePeriod + 1); // Defeat it await daoProtocolDefeatProposal(propId, index, { from: challenger }); // Claim bond on invalid index await shouldRevert(daoProtocolClaimBondChallenger(propId, [challengeIndices.phase2Indices[0]], { from: proposer }), 'Claimed invalid index', 'Invalid challenge state'); // Try to claim proposal bond await shouldRevert(daoProtocolClaimBondChallenger(propId, [1], { from: proposer }), 'Claimed proposal bond', 'Invalid challenger'); }); }); }); }); describe('With allow listed controller', () => { before(async () => { await setDAOProtocolBootstrapSettingAddressList(RocketDAOProtocolSettingsNetwork, 'network.allow.listed.controllers', [allowListed.address], { from: owner }); }); it(printTitle('random', 'fails to update UARS parameters when not on allow list'), async () => { await shouldRevert(setDaoProtocolNodeShareSecurityCouncilAdder('0.005'.ether, { from: random, }), 'Was able to update node share security council adder', 'Not on allow list'); await shouldRevert(setDaoProtocolNodeCommissionShare('0.15'.ether, { from: random, }), 'Was able to update node commission share', 'Not on allow list'); await shouldRevert(setDaoProtocolVoterShare('0.15'.ether, { from: random, }), 'Was able to update voter share', 'Not on allow list'); }); it(printTitle('allow listed', 'can update node share security council adder if on allow list'), async () => { await setDaoProtocolNodeShareSecurityCouncilAdder('0.005'.ether, { from: allowListed }); }); it(printTitle('allow listed', 'fails to update UARS parameter if removed from allow list'), async () => { await setDAOProtocolBootstrapSettingAddressList(RocketDAOProtocolSettingsNetwork, 'network.allow.listed.controllers', [], { from: owner }); await shouldRevert(setDaoProtocolNodeShareSecurityCouncilAdder('0.005'.ether, { from: allowListed, }), 'Was able to update node share security council adder', 'Not on allow list'); }); it(printTitle('allow listed', 'fails to set node share security council adder higher than max'), async () => { const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); const maximum = await rocketDAOProtocolSettingsNetwork.getMaxNodeShareSecurityCouncilAdder(); // Set to max works await setDaoProtocolNodeShareSecurityCouncilAdder(maximum, { from: allowListed }); // Set greater than max fails await shouldRevert(setDaoProtocolNodeShareSecurityCouncilAdder(maximum + '0.00001'.ether, { from: allowListed, }), 'Was able to update node share security council adder greater than max', 'Value must be <= max value'); }); it(printTitle('allow listed', 'fails to set voter share + node share > 100%'), async () => { await setDAOProtocolBootstrapSettingAddressList(RocketDAOProtocolSettingsNetwork, 'network.allow.listed.controllers', [allowListed.address], { from: owner }); // Set voter and node to 50% await setDaoProtocolNodeCommissionShare('0.5'.ether, { from: allowListed }); await setDaoProtocolVoterShare('0.5'.ether, { from: allowListed }); // Fail to then set node to 51% await shouldRevert(setDaoProtocolNodeCommissionShare('0.51'.ether, { from: allowListed, }), 'Was able to set rETH commission grater than 100%', 'rETH Commission must be <= 100%'); }); it(printTitle('allow listed', 'can update node commission share if on allow list'), async () => { await setDAOProtocolBootstrapSettingAddressList(RocketDAOProtocolSettingsNetwork, 'network.allow.listed.controllers', [allowListed.address], { from: owner }); await setDaoProtocolNodeCommissionShare('0.10'.ether, { from: allowListed }); }); it(printTitle('allow listed', 'can update voter share if on allow list'), async () => { await setDAOProtocolBootstrapSettingAddressList(RocketDAOProtocolSettingsNetwork, 'network.allow.listed.controllers', [allowListed.address], { from: owner }); await setDaoProtocolVoterShare('0.20'.ether, { from: allowListed }); }); }); }); } ================================================ FILE: test/dao/dao-protocol-treasury-tests.js ================================================ import { describe, it, before } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { bootstrapTreasuryNewContract, bootstrapTreasuryUpdateContract, } from './scenario-dao-protocol-bootstrap'; import { payOutContracts, withdrawBalance } from './scenario-dao-protocol-treasury'; import { shouldRevert } from '../_utils/testing'; import { RocketTokenRPL, RocketVault } from '../_utils/artifacts'; import { mintRPL } from '../_helpers/tokens'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketDAOProtocol', () => { let owner, recipient1, recipient2, random; const oneDay = 60 * 60 * 24; // Setup before(async () => { await globalSnapShot(); [owner, recipient1, recipient2, random] = await ethers.getSigners(); }); async function fundTreasury(amount) { await mintRPL(owner, owner, amount); const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); await rocketTokenRPL.approve(rocketVault.target, amount, {from: owner}); await rocketVault.depositToken("rocketClaimDAO", rocketTokenRPL.target, amount); } // // Start Tests // it(printTitle('guardian', 'can create a new recurring payment and update it via bootstrap'), async () => { const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract", recipient1.address, '5'.ether, oneDay, currentTime, 1, { from: owner }); await bootstrapTreasuryUpdateContract("Test contract", recipient1.address, '10'.ether, oneDay, 1, { from: owner }); }); it(printTitle('recipient', 'can not payout non-existing contract'), async () => { await shouldRevert( payOutContracts(["Invalid contract"], {from: recipient1}), 'Was able to pay out non-existing contract', 'Contract does not exist' ); }); it(printTitle('recipient', 'can payout and withdraw RPL from a recurring contract'), async () => { // Send RPL to treasury await fundTreasury('10'.ether); // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract", recipient1.address, '5'.ether, oneDay, currentTime, 2, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Execute a payout await payOutContracts(["Test contract"], {from: recipient1}); // Wait another day await helpers.time.increase(oneDay + 1); // Execute another payout await payOutContracts(["Test contract"], {from: recipient1}); // Try to withdraw 2 days worth of payments await withdrawBalance(recipient1, {from: recipient1}); }); it(printTitle('recipient', 'can payout multiple periods of a recurring payment at once'), async () => { // Send RPL to treasury await fundTreasury('20'.ether); // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract", recipient1.address, '5'.ether, oneDay, currentTime, 4, { from: owner }); // Wait 10 days await helpers.time.increase((oneDay * 10) + 1); // Payout and withdraw the 4 periods of payments await payOutContracts(["Test contract"], {from: recipient1}); const amountWithdrawn = await withdrawBalance(recipient1, {from: recipient1}); // Check result assertBN.equal(amountWithdrawn, '20'.ether, 'Unexpected amount withdrawn'); }); it(printTitle('recipient', 'can payout multiple contracts at once'), async () => { // Send RPL to treasury await fundTreasury('20'.ether); // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract 1", recipient1.address, '5'.ether, oneDay, currentTime, 1, { from: owner }); await bootstrapTreasuryNewContract("Test contract 2", recipient1.address, '10'.ether, oneDay, currentTime, 1, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Try to withdraw 4 days worth of payments await payOutContracts(["Test contract 1", "Test contract 2"], {from: recipient1}); const amountWithdrawn = await withdrawBalance(recipient1, {from: recipient1}); // Check result assertBN.equal(amountWithdrawn, '15'.ether, 'Unexpected amount withdrawn'); }); it(printTitle('recipient', 'can payout multiple contracts separately'), async () => { // Send RPL to treasury await fundTreasury('20'.ether); // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract 1", recipient1.address, '5'.ether, oneDay, currentTime, 1, { from: owner }); await bootstrapTreasuryNewContract("Test contract 2", recipient1.address, '10'.ether, oneDay, currentTime, 1, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Try to withdraw 4 days worth of payments from 1st contract await payOutContracts(["Test contract 1"], {from: recipient1}); const amountWithdrawn1 = await withdrawBalance(recipient1, {from: recipient1}); // Check result assertBN.equal(amountWithdrawn1, '5'.ether, 'Unexpected amount withdrawn'); // Try to withdraw 4 days worth of payments from 2nd contract await payOutContracts(["Test contract 2"], {from: recipient1}); const amountWithdrawn2 = await withdrawBalance(recipient1, {from: recipient1}); // Check result assertBN.equal(amountWithdrawn2, '10'.ether, 'Unexpected amount withdrawn'); }); it(printTitle('recipient', 'receives back pay when contract is updated'), async () => { // Send RPL to treasury await fundTreasury('20'.ether); // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract", recipient1.address, '5'.ether, oneDay, currentTime, 2, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Change recipient await bootstrapTreasuryUpdateContract("Test contract", recipient2.address, '5'.ether, oneDay, 2, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Payout and withdraw from original recipient and new one await payOutContracts(["Test contract"], {from: recipient1}); const amountWithdrawn1 = await withdrawBalance(recipient1, {from: recipient1}); const amountWithdrawn2 = await withdrawBalance(recipient2, {from: recipient2}); // Check result assertBN.equal(amountWithdrawn1, '5'.ether, 'Unexpected amount withdrawn'); assertBN.equal(amountWithdrawn2, '5'.ether, 'Unexpected amount withdrawn'); }); it(printTitle('recipient', 'cannot payout contract if treasury cannot afford it'), async () => { // Create a new contract for 5 RPL/day const currentTime = await helpers.time.latest(); await bootstrapTreasuryNewContract("Test contract", recipient1.address, '5'.ether, oneDay, currentTime, 1, { from: owner }); // Wait a day await helpers.time.increase(oneDay + 1); // Execute a payout await shouldRevert( payOutContracts(["Test contract"], {from: recipient1}), 'Was able to pay out greater than treasury balance', 'Insufficient treasury balance for payout' ); }); }); } ================================================ FILE: test/dao/dao-security-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { setDAOProtocolBootstrapSecurityInvite, setDAOProtocolBootstrapSetting, } from './scenario-dao-protocol-bootstrap'; import { userDeposit } from '../_helpers/deposit'; import { getDaoProtocolSecurityLeaveTime, getDaoProtocolVoteDelayTime, getDaoProtocolVotePhase1Time, getDaoProtocolVotePhase2Time, } from '../_helpers/dao'; import { daoSecurityExecute, daoSecurityMemberJoin, daoSecurityMemberLeave, daoSecurityMemberRequestLeave, daoSecurityPropose, daoSecurityVote, } from './scenario-dao-security'; import { getDepositSetting } from '../_helpers/settings'; import { RocketDAONodeTrustedProposals, RocketDAONodeTrustedUpgrade, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsSecurity, RocketDAOSecurityProposals, RocketMinipoolManager, RocketNetworkRevenues, RocketStorage, } from '../_utils/artifacts'; import * as assert from 'assert'; import { globalSnapShot } from '../_utils/snapshotting'; import { assertBN } from '../_helpers/bn'; import { compressABI } from '../_utils/contract'; import { daoNodeTrustedExecute, daoNodeTrustedPropose, daoNodeTrustedVote } from './scenario-dao-node-trusted'; import { getDAOProposalStartTime } from './scenario-dao-proposal'; import { registerNode, setNodeTrusted } from '../_helpers/node'; import { daoSecurityProposeVeto, daoSecurityUpgradeExecute, daoSecurityUpgradeVote, } from './scenario-dao-security-upgrade'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketDAOSecurity', () => { let owner, securityMember1, securityMember2, securityMember3, registeredNodeTrusted1, registeredNodeTrusted2, registeredNodeTrusted3, random; let voteDelayTime; let votePhase1Time; let votePhase2Time; let leaveTime; before(async () => { await globalSnapShot(); [ owner, securityMember1, securityMember2, securityMember3, registeredNodeTrusted1, registeredNodeTrusted2, registeredNodeTrusted3, random, ] = await ethers.getSigners(); await userDeposit({ from: random, value: '320'.ether }); voteDelayTime = await getDaoProtocolVoteDelayTime(); votePhase1Time = await getDaoProtocolVotePhase1Time(); votePhase2Time = await getDaoProtocolVotePhase2Time(); leaveTime = await getDaoProtocolSecurityLeaveTime(); }); it(printTitle('random', 'can not accept a non-existent invite'), async () => { // Accept the invitation await shouldRevert(daoSecurityMemberJoin({ from: random }), 'Was able to accept invite', 'This address has not been invited to join'); }); it(printTitle('security member', 'can accept a valid invite'), async () => { // Invite via bootstrap await setDAOProtocolBootstrapSecurityInvite('Member 1', securityMember1, { from: owner }); // Accept the invitation await daoSecurityMemberJoin({ from: securityMember1 }); }); it(printTitle('security member', 'can not leave without requesting and waiting required period'), async () => { // Invite via bootstrap await setDAOProtocolBootstrapSecurityInvite('Member 1', securityMember1, { from: owner }); // Accept the invitation await daoSecurityMemberJoin({ from: securityMember1 }); // Try to leave await shouldRevert(daoSecurityMemberLeave({ from: securityMember1 }), 'Was able to leave', 'This member has not been approved to leave or request has expired, please apply to leave again'); }); it(printTitle('security member', 'can leave after waiting required period'), async () => { // Invite via bootstrap await setDAOProtocolBootstrapSecurityInvite('Member 1', securityMember1, { from: owner }); // Accept the invitation await daoSecurityMemberJoin({ from: securityMember1 }); // Request leave await daoSecurityMemberRequestLeave({ from: securityMember1 }); // Fail to leave await shouldRevert(daoSecurityMemberLeave({ from: securityMember1 }), 'Was able to leave', 'Member has not waited required time to leave'); // Wait required time await helpers.time.increase(leaveTime + 1); // Successfully leave await daoSecurityMemberLeave({ from: securityMember1 }); }); describe('With Existing Council', () => { let rocketDAOSecurityProposals; before(async () => { // Preload contracts rocketDAOSecurityProposals = await RocketDAOSecurityProposals.deployed(); // Set up a council of 3 members await setDAOProtocolBootstrapSecurityInvite('Member 1', securityMember1, { from: owner }); await setDAOProtocolBootstrapSecurityInvite('Member 2', securityMember2, { from: owner }); await setDAOProtocolBootstrapSecurityInvite('Member 3', securityMember3, { from: owner }); await daoSecurityMemberJoin({ from: securityMember1 }); await daoSecurityMemberJoin({ from: securityMember2 }); await daoSecurityMemberJoin({ from: securityMember3 }); }) it(printTitle('security member', 'can propose and execute a valid setting change'), async () => { // Raise a proposal to disable deposits let proposalCalldata = rocketDAOSecurityProposals.interface.encodeFunctionData('proposalSettingBool', ['deposit', 'deposit.enabled', false]); // Add the proposal let proposalId = await daoSecurityPropose('Disable deposits urgently', proposalCalldata, { from: securityMember1, }); // Vote in favour await daoSecurityVote(proposalId, true, { from: securityMember1 }); await daoSecurityVote(proposalId, true, { from: securityMember2 }); // Execute await daoSecurityExecute(proposalId, { from: securityMember2 }); // Check result assert.equal(await getDepositSetting('DepositEnabled'), false, 'Deposits were not disabled'); }); it(printTitle('security member', 'can not execute a setting change on a non-approved setting path'), async () => { // Raise a proposal to increase deposit pool maximum to 10,000 ether let proposalCalldata = rocketDAOSecurityProposals.interface.encodeFunctionData('proposalSettingUint', ['deposit', 'deposit.pool.maximum', '10000'.ether]); // Add the proposal let proposalId = await daoSecurityPropose('I want more rETH!', proposalCalldata, { from: securityMember1, }); // Vote in favour await daoSecurityVote(proposalId, true, { from: securityMember1 }); await daoSecurityVote(proposalId, true, { from: securityMember2 }); // Execute await shouldRevert(daoSecurityExecute(proposalId, { from: securityMember2 }), 'Setting was changed', 'Setting is not modifiable by security council'); }); it(printTitle('security member', 'can not execute a proposal without quorum'), async () => { // Raise a proposal to disable deposits let proposalCalldata = rocketDAOSecurityProposals.interface.encodeFunctionData('proposalSettingBool', ['deposit', 'deposit.enabled', false]); // Add the proposal let proposalId = await daoSecurityPropose('Disable deposits urgently', proposalCalldata, { from: securityMember1, }); // Vote in favour await daoSecurityVote(proposalId, true, { from: securityMember1 }); // Fail to execute await shouldRevert(daoSecurityExecute(proposalId, { from: securityMember2 }), 'Proposal was executed', 'Proposal has not succeeded, has expired or has already been executed'); }); it(printTitle('security member', 'can adjust the node commission share security council adder'), async () => { // Get contracts const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); // Raise a proposal to disable deposits const adder = '0.005'.ether; let proposalCalldata = rocketDAOSecurityProposals.interface.encodeFunctionData('proposalSettingUint', ['network', 'network.node.commission.share.security.council.adder', adder]); // Add the proposal let proposalId = await daoSecurityPropose('Adjust node commission share security council adder', proposalCalldata, { from: securityMember1, }); // Vote in favour await daoSecurityVote(proposalId, true, { from: securityMember1 }); await daoSecurityVote(proposalId, true, { from: securityMember2 }); // Execute and check await daoSecurityExecute(proposalId, { from: securityMember2 }); // Check node share const effectiveNodeShare = await rocketDAOProtocolSettingsNetwork.getEffectiveNodeShare(); assertBN.equal(effectiveNodeShare, '0.05'.ether + adder); const nodeShare = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(nodeShare, '0.05'.ether + adder); // Check voter share const effectiveVoterShare = await rocketDAOProtocolSettingsNetwork.getEffectiveVoterShare(); assertBN.equal(effectiveVoterShare, '0.09'.ether - adder); const voterShare = await rocketNetworkRevenues.getCurrentVoterShare(); assertBN.equal(voterShare, '0.09'.ether - adder); }); it(printTitle('security council', 'can veto a contract upgrade'), async () => { // Get contracts const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); const rocketDAONodeTrustedUpgrade = await RocketDAONodeTrustedUpgrade.deployed(); // Set upgrade delay const upgradeDelay = 60n * 60n * 24n; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsSecurity, 'upgrade.delay', upgradeDelay, { from: owner }); // Add trusted DAO members await registerNode({ from: registeredNodeTrusted1 }); await registerNode({ from: registeredNodeTrusted2 }); await registerNode({ from: registeredNodeTrusted3 }); await setNodeTrusted(registeredNodeTrusted1, 'rocketpool-1', 'http://rocketpool.net', owner); await setNodeTrusted(registeredNodeTrusted2, 'rocketpool-2', 'http://rocketpool.net', owner); await setNodeTrusted(registeredNodeTrusted3, 'rocketpool-3', 'http://rocketpool.net', owner); await helpers.time.increase(60); // Encode the calldata for the proposal const rocketStorage = await RocketStorage.deployed(); const rocketMinipoolManagerNew = await RocketMinipoolManager.clone(rocketStorage.target); const proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalUpgrade', ['upgradeContract', 'rocketNodeManager', compressABI(RocketMinipoolManager.abi), rocketMinipoolManagerNew.target]); // Add the proposal const proposalID = await daoNodeTrustedPropose('hey guys, this is a totally safe upgrade', proposalCalldata, { from: registeredNodeTrusted1, }); // Current time const timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted1 }); // Can not execute before quorum await shouldRevert( daoNodeTrustedExecute(proposalID, { from: registeredNodeTrusted1 }), 'Was able to execute before quorum', 'Proposal has not succeeded, has expired or has already been executed' ); // Vote from second member await daoNodeTrustedVote(proposalID, true, { from: registeredNodeTrusted2 }); // Proposal has passed, we can now execute it and start the upgrade delay await daoNodeTrustedExecute(proposalID, { from: registeredNodeTrusted1 }); // Veto the upgrade let proposalId = await daoSecurityProposeVeto('veto that malicious upgrade', 1n, { from: securityMember1, }); // Vote in favour (only need 1 vote because quorum is 33% for veto) await daoSecurityUpgradeVote(proposalId, true, { from: securityMember1 }); // Execute await daoSecurityUpgradeExecute(proposalId, { from: securityMember2 }); // Check const vetoed = await rocketDAONodeTrustedUpgrade.getVetoed(1n); assert.equal(vetoed, true); // Wait for the upgrade delay await helpers.time.increase(upgradeDelay + 1n); // Executing the upgrade should fail await shouldRevert( rocketDAONodeTrustedUpgrade.connect(registeredNodeTrusted1).execute(1n), 'Was able to execute vetoed upgrade', 'Proposal has not succeeded or has been vetoed or executed' ); }); }); }); } ================================================ FILE: test/dao/scenario-dao-node-trusted-bootstrap.js ================================================ import { RocketDAONodeTrusted, RocketDAONodeTrustedUpgrade, RocketStorage, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { compressABI } from '../_utils/contract'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; // The trusted node DAO can be bootstrapped with several nodes export async function setDaoNodeTrustedBootstrapMember(_id, _url, _nodeAddress, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberID(_nodeAddress), ]).then( ([memberID]) => ({ memberID }), ); } // Set as a bootstrapped member await rocketDAONodeTrusted.connect(txOptions.from).bootstrapMember(_id, _url, _nodeAddress, txOptions); // Capture data let ds2 = await getTxData(); // Check ID has been recorded assert.strictEqual(ds2.memberID, _id, 'Member was not invited to join correctly'); } // Change a trusted node DAO setting while bootstrap mode is enabled export async function setDAONodeTrustedBootstrapSetting(_settingContractInstance, _settingPath, _value, txOptions) { // Helper function String.prototype.lowerCaseFirstLetter = function() { return this.charAt(0).toLowerCase() + this.slice(1); }; // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedSettingsContract = await _settingContractInstance.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrustedSettingsContract.getSettingUint(_settingPath), rocketDAONodeTrustedSettingsContract.getSettingBool(_settingPath), ]).then( ([settingUintValue, settingBoolValue]) => ({ settingUintValue, settingBoolValue }), ); } // Set as a bootstrapped setting. detect type first, can be a number, string or bn object if (typeof (_value) == 'number' || typeof (_value) == 'string' || typeof (_value) == 'bigint') { await rocketDAONodeTrusted.connect(txOptions.from).bootstrapSettingUint(_settingContractInstance.name.lowerCaseFirstLetter(), _settingPath, _value, txOptions); } if (typeof (_value) == 'boolean') { await rocketDAONodeTrusted.connect(txOptions.from).bootstrapSettingBool(_settingContractInstance.name.lowerCaseFirstLetter(), _settingPath, _value, txOptions); } // Capture data let ds2 = await getTxData(); // Check it was updated if (typeof (_value) == 'number' || typeof (_value) == 'string' || typeof (_value) == 'bigint') { await assertBN.equal(ds2.settingUintValue, _value, 'DAO node trusted uint256 setting not updated in bootstrap mode'); } if (typeof (_value) == 'boolean') { await assert.strictEqual(ds2.settingBoolValue, _value, 'DAO node trusted boolean setting not updated in bootstrap mode'); } } // Disable bootstrap mode export async function setDaoNodeTrustedBootstrapModeDisabled(txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getBootstrapModeDisabled(), ]).then( ([bootstrapmodeDisabled]) => ({ bootstrapmodeDisabled }), ); } // Set as a bootstrapped member await rocketDAONodeTrusted.bootstrapDisable(true, txOptions); // Capture data let ds2 = await getTxData(); // Check ID has been recorded assert.strictEqual(ds2.bootstrapmodeDisabled, true, 'Bootstrap mode was not disabled'); } // The trusted node DAO can also upgrade contracts + abi if consensus is reached export async function setDaoNodeTrustedBootstrapUpgrade(_type, _name, _abi, _contractAddress, txOptions) { // Load contracts const [ rocketStorage, rocketDAONodeTrusted, ] = await Promise.all([ RocketStorage.deployed(), RocketDAONodeTrusted.deployed(), ]); // Compress ABI if given as an array let compressedAbi = Array.isArray(_abi) ? compressABI(_abi) : _abi; // Get contract data function getContractData() { return Promise.all([ rocketStorage['getAddress(bytes32)'](ethers.solidityPackedKeccak256(['string', 'string'], ['contract.address', _name])), rocketStorage.getString(ethers.solidityPackedKeccak256(['string', 'string'], ['contract.abi', _name])), ]).then( ([address, abi]) => ({ address, abi }), ); } function getContractAddressData(_contractAddress) { return Promise.all([ rocketStorage.getBool(ethers.solidityPackedKeccak256(['string', 'address'], ['contract.exists', _contractAddress])), rocketStorage.getString(ethers.solidityPackedKeccak256(['string', 'address'], ['contract.name', _contractAddress])), ]).then( ([exists, name]) => ({ exists, name }), ); } // Get initial contract data let contract1 = await getContractData(); // Upgrade contract const rocketDAONodeTrustedUpgrade = await RocketDAONodeTrustedUpgrade.deployed(); await (await rocketDAONodeTrustedUpgrade.connect(txOptions.from).bootstrapUpgrade(_type, _name, compressedAbi, _contractAddress, txOptions)).wait(); // Get updated contract data let contract2 = await getContractData(); let [oldContractData, newContractData] = await Promise.all([ getContractAddressData(contract1.address), getContractAddressData(contract2.address), ]); // Check different assertions based on upgrade type if (_type === 'upgradeContract') { // Check contract details assert.strictEqual(contract2.address, _contractAddress, 'Contract address was not successfully upgraded'); assert.strictEqual(contract2.abi, compressedAbi, 'Contract abi was not successfully upgraded'); assert.equal(oldContractData.exists, false, 'Old contract address exists flag was not unset'); assert.strictEqual(oldContractData.name, '', 'Old contract address name was not unset'); assert.equal(newContractData.exists, true, 'New contract exists flag was not set'); assert.notEqual(newContractData.name, '', 'New contract name was not set'); } if (_type === 'addContract') { // Check contract details assert.strictEqual(contract2.address, _contractAddress, 'Contract address was not set'); assert.strictEqual(contract2.abi, compressedAbi, 'Contract abi was not successfully upgraded'); assert.equal(newContractData.exists, true, 'New contract exists flag was not set'); assert.notEqual(newContractData.name, '', 'New contract name was not set'); } if (_type === 'upgradeABI' || _type === 'addABI') { // Check ABI details assert.strictEqual(contract2.abi, compressedAbi, 'Contract abi was not successfully upgraded'); } } // A registered node attempting to join as a member due to low DAO member count export async function setDaoNodeTrustedMemberRequired(_id, _url, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberCount(), rocketTokenRPL.balanceOf(txOptions.from), rocketVault.balanceOfToken('rocketDAONodeTrustedActions', rocketTokenRPL.target), ]).then( ([memberTotal, rplBalanceBond, rplBalanceVault]) => ({ memberTotal, rplBalanceBond, rplBalanceVault }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAONodeTrusted.connect(txOptions.from).memberJoinRequired(_id, _url, txOptions); // Capture data let ds2 = await getTxData(); // Check member count has increased assertBN.equal(ds2.memberTotal, ds1.memberTotal + 1n, 'Member count has not increased'); assertBN.equal(ds2.rplBalanceVault, ds1.rplBalanceVault + ds1.rplBalanceBond, 'RocketVault address does not contain the correct RPL bond amount'); } ================================================ FILE: test/dao/scenario-dao-node-trusted.js ================================================ import { RocketDAONodeTrusted, RocketDAONodeTrustedActions, RocketDAONodeTrustedProposals, RocketDAOProposal, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { getDAOProposalState, proposalStates } from './scenario-dao-proposal'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Returns true if the address is a DAO member export async function getDAOMemberIsValid(_nodeAddress, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); return await rocketDAONodeTrusted.getMemberIsValid(_nodeAddress); } // Get the total members export async function getDAONodeMemberCount(txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); return await rocketDAONodeTrusted.getMemberCount(); } // Get the number of votes needed for a proposal to pass export async function getDAONodeProposalQuorumVotesRequired(proposalID, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); return await rocketDAONodeTrusted.getProposalQuorumVotesRequired(); } // Create a proposal for this DAO export async function daoNodeTrustedPropose(_proposalMessage, _payload, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), ]).then( ([proposalTotal]) => ({ proposalTotal }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAONodeTrustedProposals.connect(txOptions.from).propose(_proposalMessage, _payload, txOptions); // Capture data let ds2 = await getTxData(); // Get the current state, new proposal should be in pending let state = Number(await getDAOProposalState(ds2.proposalTotal)); // Check proposals assertBN.equal(ds2.proposalTotal, ds1.proposalTotal + 1n, 'Incorrect proposal total count'); assert.strictEqual(state, proposalStates.Pending, 'Incorrect proposal state, should be pending'); // Return the proposal ID return Number(ds2.proposalTotal); } // Vote on a proposal for this DAO export async function daoNodeTrustedVote(_proposalID, _vote, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), rocketDAOProposal.getState(_proposalID), rocketDAOProposal.getVotesFor(_proposalID), rocketDAOProposal.getVotesRequired(_proposalID), ]).then( ([proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired]) => ({ proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired }), ); } // Add a new proposal await rocketDAONodeTrustedProposals.connect(txOptions.from).vote(_proposalID, _vote, txOptions); // Capture data let ds2 = await getTxData(); // Check proposals if (ds2.proposalState === proposalStates.Active) { assertBN.isBelow(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is active, votes for proposal should be less than the votes required'); } if (ds2.proposalState === proposalStates.Succeeded) { assertBN.isAtLeast(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is successful, yet does not have the votes required'); } } // Cancel a proposal for this DAO export async function daoNodeTrustedCancel(_proposalID, txOptions) { // Load contracts const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Add a new proposal await rocketDAONodeTrustedProposals.connect(txOptions.from).cancel(_proposalID, txOptions); // Get the current state let state = Number(await getDAOProposalState(_proposalID)); // Check proposals assert.strictEqual(state, proposalStates.Cancelled, 'Incorrect proposal state, should be cancelled'); } // Execute a successful proposal export async function daoNodeTrustedExecute(_proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getState(_proposalID), ]).then( ([proposalState]) => ({ proposalState }), ); } // Execute a proposal await rocketDAONodeTrustedProposals.connect(txOptions.from).execute(_proposalID, txOptions); // Capture data let ds2 = await getTxData(); // Check it was updated assertBN.equal(ds2.proposalState, proposalStates.Executed, 'Proposal is not in the executed state'); } // Join the DAO after a successful invite proposal has passed export async function daoNodeTrustedMemberJoin(txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberCount(), rocketTokenRPL.balanceOf(txOptions.from), rocketVault.balanceOfToken('rocketDAONodeTrustedActions', rocketTokenRPL.target), ]).then( ([memberTotal, rplBalanceBond, rplBalanceVault]) => ({ memberTotal, rplBalanceBond, rplBalanceVault }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAONodeTrustedActions.connect(txOptions.from).actionJoin(txOptions); // Capture data let ds2 = await getTxData(); // Check member count has increased assertBN.equal(ds2.memberTotal, ds1.memberTotal + 1n, 'Member count has not increased'); assertBN.equal(ds2.rplBalanceVault, ds1.rplBalanceVault + ds1.rplBalanceBond, 'RocketVault address does not contain the correct RPL bond amount'); } // Leave the DAO after a successful leave proposal has passed export async function daoNodeTrustedMemberLeave(_rplRefundAddress, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberCount(), rocketTokenRPL.balanceOf(_rplRefundAddress), rocketVault.balanceOfToken('rocketDAONodeTrustedActions', rocketTokenRPL.target), ]).then( ([memberTotal, rplBalanceRefund, rplBalanceVault]) => ({ memberTotal, rplBalanceRefund, rplBalanceVault }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAONodeTrustedActions.connect(txOptions.from).actionLeave(_rplRefundAddress, txOptions); // Capture data let ds2 = await getTxData(); // Verify assertBN.equal(ds2.memberTotal, ds1.memberTotal - 1n, 'Member count has not decreased'); assertBN.equal(ds2.rplBalanceVault, ds1.rplBalanceVault - ds2.rplBalanceRefund, 'Member RPL refund address does not contain the correct RPL bond amount'); } // Challenger a members node to respond and signal it is still alive export async function daoNodeTrustedMemberChallengeMake(_nodeAddress, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberIsValid(_nodeAddress), rocketDAONodeTrusted.getMemberIsChallenged(_nodeAddress), ]).then( ([currentMemberStatus, memberChallengedStatus]) => ({ currentMemberStatus, memberChallengedStatus }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAONodeTrustedActions.connect(txOptions.from).actionChallengeMake(_nodeAddress, txOptions); // Capture data let ds2 = await getTxData(); // Check member count has increased assert.strictEqual(ds1.currentMemberStatus, true, 'Challenged member has had their membership removed'); assert.strictEqual(ds1.memberChallengedStatus, false, 'Challenged a member that was already challenged'); assert.strictEqual(ds2.memberChallengedStatus, true, 'Member did not become challenged'); } // Decide a challenges outcome export async function daoNodeTrustedMemberChallengeDecide(_nodeAddress, _expectedMemberStatus, txOptions) { // Load contracts const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const rocketDAONodeTrustedActions = await RocketDAONodeTrustedActions.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAONodeTrusted.getMemberIsValid(_nodeAddress), rocketDAONodeTrusted.getMemberIsChallenged(_nodeAddress), ]).then( ([currentMemberStatus, memberChallengedStatus]) => ({ currentMemberStatus, memberChallengedStatus }), ); } // Add a new proposal await rocketDAONodeTrustedActions.connect(txOptions.from).actionChallengeDecide(_nodeAddress, txOptions); // Capture data let ds2 = await getTxData(); // Check member count has increased assert.strictEqual(ds2.currentMemberStatus, _expectedMemberStatus, 'Challenged member did not become their expected status'); } ================================================ FILE: test/dao/scenario-dao-proposal.js ================================================ import { RocketDAOProposal } from '../_utils/artifacts'; // Possible states that a proposal may be in export const proposalStates = { Pending: 0, Active: 1, Cancelled: 2, Defeated: 3, Succeeded: 4, Expired: 5, Executed: 6, }; // Possible vote direction export const voteStates = { NoVote: 0, Abstain: 1, For: 2, Against: 3, AgainstWithVeto: 4, }; // Get the status of a proposal export async function getDAOProposalState(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return Number(await rocketDAOProposal.getState(proposalID)); } // Get the block a proposal can start being voted on export async function getDAOProposalStartTime(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return Number(await rocketDAOProposal.getStart(proposalID)); } // Get the block a proposal can end being voted on export async function getDAOProposalEndTime(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return Number(await rocketDAOProposal.getEnd(proposalID)); } // Get the block a proposal expires export async function getDAOProposalExpires(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return Number(await rocketDAOProposal.getExpires(proposalID)); } // Get the vote count for a proposal export async function getDAOProposalVotesFor(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return await rocketDAOProposal.getVotesFor(proposalID); } // Get the vote count against a proposal export async function getDAOProposalVotesAgainst(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return await rocketDAOProposal.getVotesAgainst(proposalID); } // Get the quroum for a proposal export async function getDAOProposalVotesRequired(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); return await rocketDAOProposal.getVotesRequired(proposalID); } ================================================ FILE: test/dao/scenario-dao-protocol-bootstrap.js ================================================ import { RocketClaimDAO, RocketDAOProtocol, RocketDAOProtocolSettingsInflation, RocketDAOProtocolSettingsRewards, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Change a protocol DAO setting while bootstrap mode is enabled export async function setDAOProtocolBootstrapSetting(_settingContractInstance, _settingPath, _value, txOptions) { // Helper function String.prototype.lowerCaseFirstLetter = function() { return this.charAt(0).toLowerCase() + this.slice(1); }; // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); const rocketDAOProtocolSettingsContract = await _settingContractInstance.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolSettingsContract.getSettingUint(_settingPath), rocketDAOProtocolSettingsContract.getSettingBool(_settingPath), rocketDAOProtocolSettingsContract.getSettingAddress(_settingPath), ]).then( ([settingUintValue, settingBoolValue, settingAddressValue]) => ({ settingUintValue, settingBoolValue, settingAddressValue }), ); } // Capture data let ds1 = await getTxData(); let contractName = _settingContractInstance.name.lowerCaseFirstLetter(); // Set as a bootstrapped setting. detect type first, can be a number, string or bn object if (ethers.isAddress(_value)) { await (await rocketDAOProtocol.bootstrapSettingAddress(contractName, _settingPath, _value, txOptions)).wait(); } else { if (typeof (_value) == 'number' || typeof (_value) == 'string' || typeof (_value) == 'bigint') await (await rocketDAOProtocol.bootstrapSettingUint(contractName, _settingPath, _value, txOptions)).wait(); if (typeof (_value) == 'boolean') await(await rocketDAOProtocol.bootstrapSettingBool(contractName, _settingPath, _value, txOptions)).wait(); } // Capture data let ds2 = await getTxData(); // Check it was updated if (ethers.isAddress(_value)) { assert.strictEqual(ds2.settingAddressValue, _value, 'DAO protocol address setting not updated in bootstrap mode'); } else { if (typeof (_value) == 'number' || typeof (_value) == 'string' || typeof (_value) == 'bigint') { assertBN.equal(ds2.settingUintValue, _value, 'DAO protocol uint256 setting not updated in bootstrap mode'); } if (typeof (_value) == 'boolean') { assert.strictEqual(ds2.settingBoolValue, _value, 'DAO protocol boolean setting not updated in bootstrap mode'); } } } // Set a contract that can claim rewards export async function setDAONetworkBootstrapRewardsClaimers(_trustedNodePerc, _protocolPerc, _nodePerc, txOptions) { // Load contracts const rocketDAOProtocol = await RocketDAOProtocol.deployed(); const rocketDAOProtocolSettingsRewards = await RocketDAOProtocolSettingsRewards.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolSettingsRewards.getRewardsClaimersPerc(), ]).then( ([rewardsClaimerPerc]) => ({ rewardsClaimerPerc }), ); } // Perform tx await rocketDAOProtocol.connect(txOptions.from).bootstrapSettingClaimers(_trustedNodePerc, _protocolPerc, _nodePerc, txOptions); // Capture data let dataSet2 = await getTxData(); // Verify assertBN.equal(dataSet2.rewardsClaimerPerc[0], _trustedNodePerc, 'Claim percentage not updated correctly'); assertBN.equal(dataSet2.rewardsClaimerPerc[1], _protocolPerc, 'Claim percentage not updated correctly'); assertBN.equal(dataSet2.rewardsClaimerPerc[2], _nodePerc, 'Claim percentage not updated correctly'); } // Change an address[] protocol dao parameter export async function setDAOProtocolBootstrapSettingAddressList(_settingContractInstance, _settingPath, _value, txOptions) { String.prototype.lowerCaseFirstLetter = function() { return this.charAt(0).toLowerCase() + this.slice(1); }; // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); const rocketDAOProtocolSettingsContract = await _settingContractInstance.deployed(); let contractName = _settingContractInstance.name.lowerCaseFirstLetter(); await (await rocketDAOProtocol.bootstrapSettingAddressList(contractName, _settingPath, _value, txOptions)).wait(); // Capture data let valueAfter = await rocketDAOProtocolSettingsContract.getSettingAddressList(_settingPath); // Check value was updated for (let i = 0; i < _value.length; ++i) { assert.equal(valueAfter[i].toLowerCase(), _value[i].toLowerCase()); } } /*** Rewards *******/ // Set the current rewards claim period in seconds export async function setRewardsClaimIntervalTime(intervalTime, txOptions) { // Set it now await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsRewards, 'rpl.rewards.claim.period.time', intervalTime, txOptions); } // Spend the DAO treasury in bootstrap mode export async function spendRewardsClaimTreasury(_invoiceID, _recipientAddress, _amount, txOptions) { // Load contracts const rocketDAOProtocol = await RocketDAOProtocol.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); const rocketVault = await RocketVault.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketVault.balanceOfToken('rocketClaimDAO', rocketTokenRPL.address), rocketTokenRPL.balanceOf(_recipientAddress), ]).then( ([daoClaimTreasuryBalance, recipientBalance]) => ({ daoClaimTreasuryBalance, recipientBalance }), ); } // Capture data let ds1 = await getTxData(); // console.log(web3.utils.fromWei(ds1.daoClaimTreasuryBalance), web3.utils.fromWei(ds1.recipientBalance), web3.utils.fromWei(_amount)); // Perform tx await rocketDAOProtocol.bootstrapSpendTreasury(_invoiceID, _recipientAddress, _amount, txOptions); // Capture data let ds2 = await getTxData(); // console.log(web3.utils.fromWei(ds2.daoClaimTreasuryBalance), web3.utils.fromWei(ds2.recipientBalance), web3.utils.fromWei(_amount)); // Verify the amount sent is correct assertBN.equal(ds2.recipientBalance, ds1.recipientBalance.add(_amount), 'Amount spent by treasury does not match recipients received amount'); } // Create a new recurring payment via bootstrap export async function bootstrapTreasuryNewContract(_contractName, _recipientAddress, _amount, _periodLength, _startTime, _numPeriods, txOptions) { // Load contracts const rocketDAOProtocol = await RocketDAOProtocol.deployed(); const rocketClaimDAO = await RocketClaimDAO.deployed(); // Perform tx await rocketDAOProtocol.bootstrapTreasuryNewContract(_contractName, _recipientAddress, _amount, _periodLength, _startTime, _numPeriods, txOptions); // Sanity check const contract = await rocketClaimDAO.getContract(_contractName); assert.strictEqual(contract.recipient, _recipientAddress); assertBN.equal(contract.amountPerPeriod, _amount, 'Invalid amount'); assert.strictEqual(Number(contract.periodLength), _periodLength); assert.strictEqual(Number(contract.numPeriods), _numPeriods); assert.strictEqual(Number(contract.lastPaymentTime), _startTime); } // Update an existing recurring payment via bootstrap export async function bootstrapTreasuryUpdateContract(_contractName, _recipientAddress, _amount, _periodLength, _numPeriods, txOptions) { // Load contracts const rocketDAOProtocol = await RocketDAOProtocol.deployed(); const rocketClaimDAO = await RocketClaimDAO.deployed(); // Perform tx await rocketDAOProtocol.bootstrapTreasuryUpdateContract(_contractName, _recipientAddress, _amount, _periodLength, _numPeriods, txOptions); // Sanity check const contract = await rocketClaimDAO.getContract(_contractName); assert.strictEqual(contract.recipient, _recipientAddress); assertBN.equal(contract.amountPerPeriod, _amount, 'Invalid amount'); assert.strictEqual(Number(contract.periodLength), _periodLength); assert.strictEqual(Number(contract.numPeriods), _numPeriods); } /*** Inflation *******/ // Set the current RPL inflation rate export async function setRPLInflationIntervalRate(yearlyInflationPerc, txOptions) { // Calculate the inflation rate per day let dailyInflation = ((1 + yearlyInflationPerc) ** (1 / (365))).toFixed(18).ether; // Set it now await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsInflation, 'rpl.inflation.interval.rate', dailyInflation, txOptions); } // Set the current RPL inflation block interval export async function setRPLInflationStartTime(startTime, txOptions) { // Set it now await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsInflation, 'rpl.inflation.interval.start', startTime, txOptions); } // Disable bootstrap mode export async function setDaoProtocolBootstrapModeDisabled(txOptions) { // Load contracts const rocketDAOProtocol = await RocketDAOProtocol.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocol.getBootstrapModeDisabled(), ]).then( ([bootstrapmodeDisabled]) => ({ bootstrapmodeDisabled }), ); } // Capture data let ds1 = await getTxData(); // Set as a bootstrapped member await rocketDAOProtocol.bootstrapDisable(true, txOptions); // Capture data let ds2 = await getTxData(); // Check ID has been recorded assert.strictEqual(ds2.bootstrapmodeDisabled, true, 'Bootstrap mode was not disabled'); } // Change multiple trusted node DAO settings while bootstrap mode is enabled export async function setDAOProtocolBootstrapSettingMulti(_settingContractInstances, _settingPaths, _values, txOptions) { // Helper function String.prototype.lowerCaseFirstLetter = function() { return this.charAt(0).toLowerCase() + this.slice(1); }; // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); const contractNames = []; const values = []; const types = []; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); for (let i = 0; i < _settingContractInstances.length; i++) { const value = _values[i]; contractNames.push(_settingContractInstances[i].name.lowerCaseFirstLetter()); if (ethers.isAddress(value)) { values.push(abiCoder.encode(['address'], [value])); types.push(2); } else { if (typeof (value) == 'number' || typeof (value) == 'string' || typeof (value) == 'bigint') { values.push(abiCoder.encode(['uint256'], [value])); types.push(0); } else if (typeof (value) == 'boolean') { values.push(abiCoder.encode(['bool'], [value])); types.push(1); } else { throw new Error('Invalid value supplied'); } } } // Set as a bootstrapped setting. detect type first, can be a number, string or bn object await rocketDAOProtocol.bootstrapSettingMulti(contractNames, _settingPaths, types, values, txOptions); // Get data about the tx async function getTxData() { const instances = await Promise.all(_settingContractInstances.map(instance => instance.deployed())); return Promise.all(instances.map((rocketDAOProtocolSettingsContract, index) => { switch (types[index]) { case 0: return rocketDAOProtocolSettingsContract.getSettingUint(_settingPaths[index]); case 1: return rocketDAOProtocolSettingsContract.getSettingBool(_settingPaths[index]); case 2: return rocketDAOProtocolSettingsContract.getSettingAddress(_settingPaths[index]); } })); } // Capture data let data = await getTxData(); // Check it was updated for (let i = 0; i < _values.length; i++) { const value = _values[i]; switch (types[i]) { case 0: assertBN.equal(data[i], value, 'DAO protocol uint256 setting not updated in bootstrap mode'); break; case 1: assert.strictEqual(data[i], value, 'DAO protocol boolean setting not updated in bootstrap mode'); break; case 2: assert.strictEqual(data[i], value, 'DAO protocol address setting not updated in bootstrap mode'); break; } } } export async function setDAOProtocolBootstrapEnableGovernance(txOptions) { // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); // Execute enable transaction await rocketDAOProtocol.bootstrapEnableGovernance(txOptions); } /*** Security council *******/ // Use bootstrap power to invite a member to the security council export async function setDAOProtocolBootstrapSecurityInvite(_id, _memberAddress, txOptions) { // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); // Execute the invite await rocketDAOProtocol.bootstrapSecurityInvite(_id, _memberAddress, txOptions); } // Use bootstrap power to kick a member from the security council export async function setDAOProtocolBootstrapSecurityKick(_id, _memberAddress, txOptions) { // Load contracts const rocketDAOProtocol = (await RocketDAOProtocol.deployed()).connect(txOptions.from); // Execute the kick await rocketDAOProtocol.bootstrapSecurityKick(_memberAddress, txOptions); } ================================================ FILE: test/dao/scenario-dao-protocol-treasury.js ================================================ import { RocketClaimDAO, RocketTokenRPL } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); export async function payOutContracts(_contractNames, txOptions) { // Load contracts const rocketClaimDAO = await RocketClaimDAO.deployed(); // Calculate expected payouts let contracts = []; let expectedPayouts = {}; for (const name of _contractNames) { contracts.push(await rocketClaimDAO.getContract(name)); } const currentTime = await helpers.time.latest(); for (const contract of contracts) { const lastPaymentTime = Number(contract.lastPaymentTime); const periodLength = Number(contract.periodLength); const numPeriods = Number(contract.numPeriods); const periodsPaid = Number(contract.periodsPaid); if (periodsPaid >= numPeriods) { continue; } let periodsToPay = Math.floor((currentTime - lastPaymentTime) / periodLength); if (periodsToPay + periodsPaid > numPeriods) { periodsToPay = numPeriods - periodsPaid; } const expectedPayout = contract.amountPerPeriod * BigInt(periodsToPay); if (!expectedPayouts.hasOwnProperty(contract.recipient)) { expectedPayouts[contract.recipient] = 0n; } expectedPayouts[contract.recipient] = expectedPayouts[contract.recipient] + expectedPayout; } async function getBalances() { let balances = {}; for (const address in expectedPayouts) { balances[address] = await rocketClaimDAO.getBalance(address); } return balances; } // Record balances before, execute, record balances after const balancesBefore = await getBalances(); await rocketClaimDAO.connect(txOptions.from).payOutContracts(_contractNames, txOptions); const balancesAfter = await getBalances(); // Check balance deltas for (const address in expectedPayouts) { const delta = balancesAfter[address] - balancesBefore[address]; assertBN.equal(delta, expectedPayouts[address], 'Unexpected change in balance'); } } export async function withdrawBalance(recipient, txOptions) { // Load contracts const rocketClaimDAO = await RocketClaimDAO.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); // Get balance before withdrawal const balanceBefore = await rocketClaimDAO.getBalance(recipient); const tokenBalanceBefore = await rocketTokenRPL.balanceOf(recipient); // Withdraw await rocketClaimDAO.connect(txOptions.from).withdrawBalance(recipient, txOptions); // Check change in balances const balanceAfter = await rocketClaimDAO.getBalance(recipient); const tokenBalanceAfter = await rocketTokenRPL.balanceOf(recipient); assertBN.equal(balanceAfter, 0n, 'Balance did not zero'); assertBN.equal(tokenBalanceAfter - tokenBalanceBefore, balanceBefore, 'Unexpected change in RPL balance'); return balanceBefore; } ================================================ FILE: test/dao/scenario-dao-protocol.js ================================================ import { RocketDAOProtocolProposal, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsProposals, RocketDAOProtocolVerifier, RocketNetworkVoting, RocketNodeManager, RocketNodeStaking, RocketTokenRPL, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { voteStates } from './scenario-dao-proposal'; import { shouldRevert } from '../_utils/testing'; import { getDaoProtocolProposalBond } from '../_helpers/dao'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Possible states that a proposal may be in export const proposalStates = { Pending: 0, ActivePhase1: 1, ActivePhase2: 2, Cancelled: 3, Vetoed: 4, QuorumNotMet: 5, Defeated: 6, Succeeded: 7, Expired: 8, Executed: 9, }; // Get the status of a proposal export async function getDAOProposalState(proposalID) { // Load contracts const rocketDAOProposal = await RocketDAOProtocolProposal.deployed(); return await rocketDAOProposal.getState(proposalID); } // Get the quorum for a proposal export async function getDAOProposalVotesRequired(proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProtocolProposal.deployed(); return await rocketDAOProposal.getVotingPowerRequired(proposalID); } /** * Returns an array of voting power for each node in the protocol at the given block */ export async function getDelegatedVotingPower(block) { // Load contracts const rocketNetworkVoting = await RocketNetworkVoting.deployed(); const rocketNodeManager = await RocketNodeManager.deployed(); // Grab the number of nodes at the block const nodeCount = await rocketNetworkVoting.getNodeCount(block); // Setup data structs for calculation const delegatedPower = []; const delegateIndices = {}; const addresses = []; const votingPower = []; // Loop over each node and collect their delegate and voting power for (let i = 0; i < nodeCount; i++) { const nodeAddress = await rocketNodeManager.getNodeAt(i); addresses[i] = nodeAddress; const power = await rocketNetworkVoting.getVotingPower(nodeAddress, block); const delegate = await rocketNetworkVoting.getDelegate(nodeAddress, block); delegatedPower.push({ nodeAddress, power, delegate, }); delegateIndices[nodeAddress] = i; votingPower[i] = 0n; } // Loop over the nodes again and compute final delegated voting power for (let i = 0; i < nodeCount; i++) { const delegateAddress = addresses[i]; for (let j = 0; j < nodeCount; j++) { if (delegatedPower[j].delegate === delegateAddress) { votingPower[i] = votingPower[i] + delegatedPower[j].power; } } } return votingPower; } export async function getPhase2VotingPower(block, nodeIndex) { // Load contracts const rocketNetworkVoting = await RocketNetworkVoting.deployed(); const rocketNodeManager = await RocketNodeManager.deployed(); // Grab the number of nodes at the block const nodeCount = Number(await rocketNetworkVoting.getNodeCount(block)); // Setup data structs for calculation const delegatedPower = []; const delegateIndices = {}; const votingPower = []; // Loop over each node and collect their delegate and voting power for (let i = 0; i < nodeCount; i++) { const nodeAddress = await rocketNodeManager.getNodeAt(i); const power = await rocketNetworkVoting.getVotingPower(nodeAddress, block); const delegate = await rocketNetworkVoting.getDelegate(nodeAddress, block); delegatedPower.push({ nodeAddress, power, delegate, }); delegateIndices[nodeAddress] = i; } const nodeAddress = await rocketNodeManager.getNodeAt(nodeIndex); // Loop over the nodes again and sum voting power for given node index for (let i = 0; i < nodeCount; i++) { if (delegatedPower[i].delegate === nodeAddress) { votingPower.push(delegatedPower[i].power); } else { votingPower.push(0n); } } return votingPower; } export function constructTreeLeaves(votingPower) { // Collect voting power const nodeCount = votingPower.length; if (nodeCount === 0) { return []; } const subDepth = Math.ceil(Math.log2(nodeCount)); const leafCount = 2 ** subDepth; let tree = []; for (let j = 0; j < leafCount; j++) { let balance = 0n; if (j < nodeCount) { balance = votingPower[j]; } tree.push({ hash: ethers.solidityPackedKeccak256(['uint256'], [balance]), sum: balance, }); } return tree; } export function getDepthFromIndex(index) { return Math.floor(Math.log2(index)); } export function cloneLeaves(leaves) { var ret = []; for (const leaf of leaves) { ret.push({ hash: leaf.hash, sum: leaf.sum.toString().BN, }); } return ret; } export function daoProtocolGenerateVoteProof(leaves, index) { // Create copy as we mutate it leaves = cloneLeaves(leaves); const sum = leaves[index].sum; const depth = Math.log2(leaves.length); index += 2 ** depth; const offset = getDepthFromIndex(index); // Build a proof from the challenged node up to the root node const proof = []; for (let level = offset; level > 0; level--) { let n = 2 ** level; for (let i = 0; i < n / 2; i++) { const a = i * 2; const b = a + 1; const indexOffset = 2 ** level; if (indexOffset + a === index) { proof.push(leaves[b]); } else if (indexOffset + b === index) { proof.push(leaves[a]); } leaves[i] = { hash: ethers.solidityPackedKeccak256( ['bytes32', 'uint256', 'bytes32', 'uint256'], [leaves[a].hash, leaves[a].sum, leaves[b].hash, leaves[b].sum], ), sum: leaves[a].sum + leaves[b].sum, }; } index = Math.floor(index / 2); } return { sum: sum, witness: proof, }; } export function daoProtocolGenerateChallengeProof(leaves, order, index = 1) { // Create copy as we mutate it leaves = cloneLeaves(leaves); let node; let offset = getDepthFromIndex(index); // Total depth of the tree const depth = Math.log2(leaves.length); // Walk up the merkle tree until we get to the offset height for (let level = depth; level > offset; level--) { let n = 2 ** level; for (let i = 0; i < n / 2; i++) { const a = i * 2; const b = a + 1; leaves[i] = { hash: ethers.solidityPackedKeccak256( ['bytes32', 'uint256', 'bytes32', 'uint256'], [leaves[a].hash, leaves[a].sum, leaves[b].hash, leaves[b].sum], ), sum: leaves[a].sum + leaves[b].sum, }; } } // Save the challenged node const nodeOffset = index - (2 ** offset); node = leaves[nodeOffset]; // Build a proof from the challenged node up to the root node const proof = []; for (let level = offset; level > 0; level--) { let n = 2 ** level; for (let i = 0; i < n / 2; i++) { const a = i * 2; const b = a + 1; const indexOffset = 2 ** level; if (indexOffset + a === index) { proof.push(leaves[b]); } else if (indexOffset + b === index) { proof.push(leaves[a]); } leaves[i] = { hash: ethers.solidityPackedKeccak256( ['bytes32', 'uint256', 'bytes32', 'uint256'], [leaves[a].hash, leaves[a].sum, leaves[b].hash, leaves[b].sum], ), sum: leaves[a].sum + leaves[b].sum, }; } index = Math.floor(index / 2); } let proofLength = order; // On last round, proof may be shorter if (offset === depth) { proofLength = depth % order; } if (proofLength === 0) { proofLength = order; } return { node: node, proof: proof.slice(0, proofLength), }; } // Construct a merkle tree pollard of a merkle sum tree of effective RPL stake to submit with a proposal export async function daoProtocolGeneratePollard(leaves, order, index = 1) { // Create copy as we mutate it leaves = cloneLeaves(leaves); let nodes = []; const offset = getDepthFromIndex(index); // Total depth of the tree const depth = Math.log2(leaves.length); if (order + offset > depth) { order -= (order + offset) - depth; } // Calculate pollard parameters const pollardSize = 2 ** order; const pollardDepth = offset + order; const pollardOffset = index * (2 ** order) - (2 ** (order + offset)); // The if (depth === pollardDepth) { nodes = leaves.slice(pollardOffset, pollardOffset + pollardSize); } // Walk up the merkle tree until we get to the offset height for (let level = depth; level > offset; level--) { let n = 2 ** level; for (let i = 0; i < n / 2; i++) { const a = i * 2; const b = a + 1; leaves[i] = { hash: ethers.solidityPackedKeccak256( ['bytes32', 'uint256', 'bytes32', 'uint256'], [leaves[a].hash, leaves[a].sum, leaves[b].hash, leaves[b].sum], ), sum: leaves[a].sum + leaves[b].sum, }; } // Slice out the nodes for the pollard if (level - 1 === offset + order) { nodes = leaves.slice(pollardOffset, pollardOffset + pollardSize); } } return nodes; } export function getSubIndex(globalIndex, leaves) { // Total depth of the subtree const depth = Math.log2(leaves.length); // Total depth of the extended tree const maxDepth = depth * 2; // Global depth of the given index const globalDepth = getDepthFromIndex(globalIndex); // Depth of the given index into the subtree const phase2IndexDepth = globalDepth - depth; // Global root index of the subtree const phase2RootIndex = Math.floor(globalIndex / (2 ** phase2IndexDepth)); // Subtree index of the given index const n = 2 ** phase2IndexDepth; return globalIndex - (phase2RootIndex * n) + n; } // Create a proposal for this DAO export async function daoProtocolPropose(_proposalMessage, _payload, _block, _treeNodes, txOptions) { // Create local copy const treeNodes = []; // Load contracts // const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOProtocolProposal = await RocketDAOProtocolProposal.deployed(); const rocketDAOProtocolSettingsProposal = await RocketDAOProtocolSettingsProposals.deployed(); const proposalQuorum = await rocketDAOProtocolSettingsProposal.getProposalQuorum(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolProposal.getTotal(), ]).then( ([proposalTotal]) => ({ proposalTotal }), ); } // Capture data let ds1 = await getTxData(); // Convert BNs to strings and calculate quorum let quorum = 0n; for (let i = 0; i < _treeNodes.length; i++) { quorum = quorum + _treeNodes[i].sum; treeNodes[i] = { sum: _treeNodes[i].sum.toString(), hash: _treeNodes[i].hash, }; } quorum = quorum * proposalQuorum / '1'.ether; await rocketDAOProtocolProposal.connect(txOptions.from).propose(_proposalMessage, _payload, _block, treeNodes, txOptions); // Capture data let ds2 = await getTxData(); // Get the current state, new proposal should be in pending let state = Number(await getDAOProposalState(ds2.proposalTotal)); let votesRequired = await getDAOProposalVotesRequired(ds2.proposalTotal); // Check proposals assertBN.equal(ds2.proposalTotal, ds1.proposalTotal + 1n, 'Incorrect proposal total count'); assert.strictEqual(state, proposalStates.Pending, 'Incorrect proposal state, should be pending'); assertBN.equal(votesRequired, quorum, 'Incorrect votes required'); // Return the proposal ID return Number(ds2.proposalTotal); } export async function daoProtocolCreateChallenge(_proposalID, _index, _node, _witness, txOptions) { _node.sum = _node.sum.toString(); _witness = _witness.slice(); for (let i = 0; i < _witness.length; i++) { _witness[i].sum = _witness[i].sum.toString(); } // Load contracts const rocketDAOProtocolVerifier = (await RocketDAOProtocolVerifier.deployed()).connect(txOptions.from); // Create the challenge await rocketDAOProtocolVerifier.createChallenge(_proposalID, _index, _node, _witness, txOptions); } export async function daoProtocolDefeatProposal(_proposalID, _index, txOptions) { // Load contracts const rocketDAOProtocolVerifier = (await RocketDAOProtocolVerifier.deployed()).connect(txOptions.from); // Create the challenge await rocketDAOProtocolVerifier.defeatProposal(_proposalID, _index, txOptions); } export async function daoProtocolSubmitRoot(_proposalID, _index, _treeNodes, txOptions) { _treeNodes = cloneLeaves(_treeNodes); // Load contracts const rocketDAOProtocolVerifier = (await RocketDAOProtocolVerifier.deployed()).connect(txOptions.from); // Convert BN to strings for (let i = 0; i < _treeNodes.length; i++) { _treeNodes[i].sum = _treeNodes[i].sum.toString(); } // Create the challenge await rocketDAOProtocolVerifier.submitRoot(_proposalID, _index, _treeNodes, txOptions); } // Vote on a proposal for this DAO export async function daoProtocolVote(_proposalID, _vote, _votingPower, _nodeIndex, _witness, txOptions) { // Load contracts const rocketDAOProtocolProposal = await RocketDAOProtocolProposal.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolProposal.getTotal(), rocketDAOProtocolProposal.getState(_proposalID), rocketDAOProtocolProposal.getVotingPowerFor(_proposalID), rocketDAOProtocolProposal.getVotingPowerRequired(_proposalID), rocketDAOProtocolProposal.getVotingPowerAgainst(_proposalID), rocketDAOProtocolProposal.getVotingPowerVeto(_proposalID), rocketDAOProtocolProposal.getReceiptDirection(_proposalID, txOptions.from), ]).then( ([proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired, proposalVotesAgainst, proposalVotesVeto, direction]) => ({ proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired, proposalVotesAgainst, proposalVotesVeto, direction: direction, }), ); } _witness = _witness.slice(); for (let i = 0; i < _witness.length; i++) { _witness[i].sum = _witness[i].sum.toString(); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAOProtocolProposal.connect(txOptions.from).vote(_proposalID, _vote, _votingPower, _nodeIndex, _witness, txOptions); // Capture data let ds2 = await getTxData(); // Check proposals if (ds2.proposalState === proposalStates.Active) { assertBN.isBelow(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is active, votes for proposal should be less than the votes required'); } if (ds2.proposalState === proposalStates.Succeeded) { assertBN.isAtLeast(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is successful, yet does not have the votes required'); } const forDelta = ds2.proposalVotesFor - ds1.proposalVotesFor; const againstDelta = ds2.proposalVotesAgainst - ds1.proposalVotesAgainst; const vetoDelta = ds2.proposalVotesVeto - ds1.proposalVotesVeto; if (_vote === voteStates.For) { assertBN.equal(forDelta, _votingPower); assertBN.equal(againstDelta, 0n); assertBN.equal(vetoDelta, 0n); } else if (_vote === voteStates.Against) { assertBN.equal(forDelta, 0n); assertBN.equal(againstDelta, _votingPower); assertBN.equal(vetoDelta, 0n); } else if (_vote === voteStates.AgainstWithVeto) { assertBN.equal(forDelta, 0n); assertBN.equal(againstDelta, _votingPower); assertBN.equal(vetoDelta, _votingPower); } else { assertBN.equal(forDelta, 0n); assertBN.equal(againstDelta, 0n); assertBN.equal(vetoDelta, 0n); } } // Override vote on a proposal for this DAO export async function daoProtocolOverrideVote(_proposalID, _vote, txOptions) { // Load contracts const rocketDAOProtocolProposal = (await RocketDAOProtocolProposal.deployed()).connect(txOptions.from); const rocketNetworkVoting = await RocketNetworkVoting.deployed(); const proposalBlock = await rocketDAOProtocolProposal.getProposalBlock(_proposalID); const delegate = await rocketNetworkVoting.getDelegate(txOptions.from, proposalBlock); const votingPower = await rocketNetworkVoting.getVotingPower(txOptions.from, proposalBlock); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolProposal.getTotal(), rocketDAOProtocolProposal.getState(_proposalID), rocketDAOProtocolProposal.getVotingPowerFor(_proposalID), rocketDAOProtocolProposal.getVotingPowerRequired(_proposalID), rocketDAOProtocolProposal.getVotingPowerAgainst(_proposalID), rocketDAOProtocolProposal.getVotingPowerVeto(_proposalID), rocketDAOProtocolProposal.getReceiptDirection(_proposalID, txOptions.from), rocketDAOProtocolProposal.getReceiptDirection(_proposalID, delegate), rocketDAOProtocolProposal.getReceiptHasVotedPhase1(_proposalID, delegate), ]).then( ([proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired, proposalVotesAgainst, proposalVotesVeto, direction, delegateDirection, delegateVotedPhase1]) => ({ proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired, proposalVotesAgainst, proposalVotesVeto, direction: Number(direction), delegateDirection: Number(delegateDirection), delegateVotedPhase1, }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal if (ds1.delegateVotedPhase1 && _vote === ds1.delegateDirection) { await shouldRevert(rocketDAOProtocolProposal.overrideVote(_proposalID, _vote, txOptions), 'Vote was accepted', 'Vote direction is the same as delegate'); return; } else { await rocketDAOProtocolProposal.overrideVote(_proposalID, _vote, txOptions); } // Capture data let ds2 = await getTxData(); const forDelta = ds2.proposalVotesFor - ds1.proposalVotesFor; const againstDelta = ds2.proposalVotesAgainst - ds1.proposalVotesAgainst; const vetoDelta = ds2.proposalVotesVeto - ds1.proposalVotesVeto; let expectedForDelta, expectedAgainstDelta; let expectedVetoDelta = 0n; if (!ds1.delegateVotedPhase1) { if (_vote === voteStates.For) { expectedForDelta = votingPower; expectedAgainstDelta = 0n; } else if (_vote === voteStates.Against) { expectedForDelta = 0n; expectedAgainstDelta = votingPower; } else if (_vote === voteStates.AgainstWithVeto) { expectedForDelta = 0n; expectedAgainstDelta = votingPower; expectedVetoDelta = votingPower; } else if (_vote === voteStates.Abstain) { expectedForDelta = 0n; expectedAgainstDelta = 0n; } } else if (ds1.delegateDirection === voteStates.For) { expectedForDelta = -votingPower; if (_vote !== voteStates.Abstain) { expectedAgainstDelta = votingPower; } if (_vote === voteStates.AgainstWithVeto) { expectedVetoDelta = votingPower; } } else if (ds1.delegateDirection === voteStates.Abstain) { if (_vote !== voteStates.For) { expectedForDelta = votingPower; } else if (_vote !== voteStates.Against) { expectedAgainstDelta = votingPower; } else { expectedAgainstDelta = votingPower; expectedVetoDelta = votingPower; } } else { expectedAgainstDelta = -votingPower; if (ds1.delegateDirection === voteStates.AgainstWithVeto) { expectedVetoDelta = -votingPower; } if (_vote !== voteStates.For) { expectedForDelta = votingPower; } } assertBN.equal(forDelta, expectedForDelta); assertBN.equal(againstDelta, expectedAgainstDelta); assertBN.equal(vetoDelta, expectedVetoDelta); } // Cancel a proposal for this DAO export async function daoProtocolCancel(_proposalID, txOptions) { // Load contracts const rocketDAOProtocolProposal = await RocketDAOProtocolProposal.deployed(); // Add a new proposal await rocketDAOProtocolProposal.cancel(_proposalID, txOptions); // Get the current state let state = Number(await getDAOProposalState(_proposalID)); // Check proposals assert.strictEqual(state, proposalStates.Cancelled, 'Incorrect proposal state, should be cancelled'); } // Execute a successful proposal export async function daoProtocolExecute(_proposalID, txOptions) { // Load contracts const rocketDAOProtocolProposal = (await RocketDAOProtocolProposal.deployed()).connect(txOptions.from); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolProposal.getState(_proposalID), ]).then( ([proposalState]) => ({ proposalState }), ); } // Execute a proposal await rocketDAOProtocolProposal.execute(_proposalID, txOptions); // Capture data let ds2 = await getTxData(); // Check it was updated assertBN.equal(ds2.proposalState, proposalStates.Executed, 'Proposal is not in the executed state'); } // Finalise a vetoed proposal export async function daoProtocolFinalise(_proposalID, txOptions) { // Load contracts const rocketDAOProtocolProposal = (await RocketDAOProtocolProposal.deployed()).connect(txOptions.from); const rocketNodeStaking = await RocketNodeStaking.deployed(); const proposer = await rocketDAOProtocolProposal.getProposer(_proposalID); const proposalBond = await getDaoProtocolProposalBond(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProtocolProposal.getState(_proposalID), rocketDAOProtocolProposal.getFinalised(_proposalID), rocketNodeStaking.getNodeLockedRPL(proposer), rocketNodeStaking.getNodeStakedRPL(proposer), ]).then( ([proposalState, finalised, lockedRPL, stakedRPL]) => ({ proposalState, finalised, lockedRPL, stakedRPL }), ); } // Capture data let ds1 = await getTxData(); // Execute a proposal await rocketDAOProtocolProposal.finalise(_proposalID, txOptions); // Capture data let ds2 = await getTxData(); const lockedDelta = ds2.lockedRPL - ds1.lockedRPL; const stakedDelta = ds2.stakedRPL - ds1.stakedRPL; // Check for bond burn of proposals staked RPL assertBN.equal(-lockedDelta, proposalBond); assertBN.equal(-stakedDelta, proposalBond); assert.equal(ds2.finalised, true); } export async function daoProtocolClaimBondProposer(_proposalID, _indices, txOptions) { const rocketDAOProtocolVerifier = (await RocketDAOProtocolVerifier.deployed()).connect(txOptions.from); const rocketNodeStaking = (await RocketNodeStaking.deployed()).connect(txOptions.from); const rocketTokenRPL = await RocketTokenRPL.deployed(); const lockedBalanceBefore = await rocketNodeStaking.getNodeLockedRPL(txOptions.from); const balanceBefore = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); const supplyBefore = await rocketTokenRPL.totalSupply(); await rocketDAOProtocolVerifier.claimBondProposer(_proposalID, _indices, txOptions); const lockedBalanceAfter = await rocketNodeStaking.getNodeLockedRPL(txOptions.from); const balanceAfter = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); const supplyAfter = await rocketTokenRPL.totalSupply(); return { staked: balanceAfter - balanceBefore, locked: lockedBalanceAfter - lockedBalanceBefore, burned: supplyBefore - supplyAfter, }; } export async function daoProtocolClaimBondChallenger(_proposalID, _indices, txOptions) { const rocketDAOProtocolVerifier = (await RocketDAOProtocolVerifier.deployed()).connect(txOptions.from); const rocketNodeStaking = await RocketNodeStaking.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); const lockedBalanceBefore = await rocketNodeStaking.getNodeLockedRPL(txOptions.from); const balanceBefore = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); const supplyBefore = await rocketTokenRPL.totalSupply(); await rocketDAOProtocolVerifier.claimBondChallenger(_proposalID, _indices, txOptions); const lockedBalanceAfter = await rocketNodeStaking.getNodeLockedRPL(txOptions.from); const balanceAfter = await rocketNodeStaking.getNodeStakedRPL(txOptions.from); const supplyAfter = await rocketTokenRPL.totalSupply(); return { staked: balanceAfter - balanceBefore, locked: lockedBalanceAfter - lockedBalanceBefore, burned: supplyBefore - supplyAfter, }; } export async function setDaoProtocolNodeShareSecurityCouncilAdder(_value, txOptions) { const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); await rocketDAOProtocolSettingsNetwork.connect(txOptions.from).setNodeShareSecurityCouncilAdder(_value); // Check value was updated const valueAfter = await rocketDAOProtocolSettingsNetwork.getNodeShareSecurityCouncilAdder() assertBN.equal(valueAfter, _value); } export async function setDaoProtocolNodeCommissionShare(_value, txOptions) { const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); await rocketDAOProtocolSettingsNetwork.connect(txOptions.from).setNodeCommissionShare(_value); // Check value was updated const valueAfter = await rocketDAOProtocolSettingsNetwork.getNodeShare() assertBN.equal(valueAfter, _value); } export async function setDaoProtocolVoterShare(_value, txOptions) { const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); await rocketDAOProtocolSettingsNetwork.connect(txOptions.from).setVoterShare(_value); // Check value was updated const valueAfter = await rocketDAOProtocolSettingsNetwork.getVoterShare() assertBN.equal(valueAfter, _value); } ================================================ FILE: test/dao/scenario-dao-security-upgrade.js ================================================ import { RocketDAOProposal, RocketDAOSecurity, RocketDAOSecurityActions, RocketDAOSecurityProposals, RocketDAOSecurityUpgrade, } from '../_utils/artifacts'; import { getDAOProposalState, proposalStates } from './scenario-dao-proposal'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Create a veto proposal for this DAO export async function daoSecurityProposeVeto(_proposalMessage, _proposalId, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityUpgrade = await RocketDAOSecurityUpgrade.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), ]).then( ([proposalTotal]) => ({ proposalTotal }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAOSecurityUpgrade.connect(txOptions.from).proposeVeto(_proposalMessage, _proposalId, txOptions); // Capture data let ds2 = await getTxData(); // Get the current state, new proposal should be in pending let state = Number(await getDAOProposalState(ds2.proposalTotal)); // Check proposals assertBN.equal(ds2.proposalTotal, ds1.proposalTotal + 1n, 'Incorrect proposal total count'); assert.strictEqual(state, proposalStates.Pending, 'Incorrect proposal state, should be pending'); // Return the proposal ID return Number(ds2.proposalTotal); } // Vote on a proposal for this DAO export async function daoSecurityUpgradeVote(_proposalID, _vote, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityUpgrade = await RocketDAOSecurityUpgrade.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), rocketDAOProposal.getState(_proposalID), rocketDAOProposal.getVotesFor(_proposalID), rocketDAOProposal.getVotesRequired(_proposalID), ]).then( ([proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired]) => ({ proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired }), ); } // Add a new proposal await rocketDAOSecurityUpgrade.connect(txOptions.from).vote(_proposalID, _vote, txOptions); // Capture data let ds2 = await getTxData(); // Check proposals if (ds2.proposalState === proposalStates.Active) { assertBN.isBelow(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is active, votes for proposal should be less than the votes required'); } if (ds2.proposalState === proposalStates.Succeeded) { assertBN.isAtLeast(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is successful, yet does not have the votes required'); } } // Execute a successful proposal export async function daoSecurityUpgradeExecute(_proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityUpgrade = await RocketDAOSecurityUpgrade.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getState(_proposalID), ]).then( ([proposalState]) => ({ proposalState }), ); } // Execute a proposal await rocketDAOSecurityUpgrade.connect(txOptions.from).execute(_proposalID, txOptions); // Capture data let ds2 = await getTxData(); // Check it was updated assertBN.equal(ds2.proposalState, proposalStates.Executed, 'Proposal is not in the executed state'); } ================================================ FILE: test/dao/scenario-dao-security.js ================================================ import { RocketDAOProposal, RocketDAOSecurity, RocketDAOSecurityActions, RocketDAOSecurityProposals, RocketDAOSecurityUpgrade, } from '../_utils/artifacts'; import { getDAOProposalState, proposalStates } from './scenario-dao-proposal'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Returns true if the address is a DAO member export async function getDAOSecurityMemberIsValid(_nodeAddress) { // Load contracts const rocketDAOSecurity = await RocketDAOSecurity.deployed(); return await rocketDAOSecurity.getMemberIsValid(_nodeAddress); } // Get the total members export async function getDAOSecurityMemberCount() { // Load contracts const rocketDAOSecurity = await RocketDAOSecurity.deployed(); return await rocketDAOSecurity.getMemberCount(); } // Get the number of votes needed for a proposal to pass export async function getDAOSecurityProposalQuorumVotesRequired(proposalID) { // Load contracts const rocketDAOSecurity = await RocketDAOSecurity.deployed(); return await rocketDAOSecurity.getProposalQuorumVotesRequired(); } // Create a proposal for this DAO export async function daoSecurityPropose(_proposalMessage, _payload, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityProposals = await RocketDAOSecurityProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), ]).then( ([proposalTotal]) => ({ proposalTotal }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAOSecurityProposals.connect(txOptions.from).propose(_proposalMessage, _payload, txOptions); // Capture data let ds2 = await getTxData(); // Get the current state, new proposal should be in pending let state = Number(await getDAOProposalState(ds2.proposalTotal)); // Check proposals assertBN.equal(ds2.proposalTotal, ds1.proposalTotal + 1n, 'Incorrect proposal total count'); assert.strictEqual(state, proposalStates.Pending, 'Incorrect proposal state, should be pending'); // Return the proposal ID return Number(ds2.proposalTotal); } // Vote on a proposal for this DAO export async function daoSecurityVote(_proposalID, _vote, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityProposals = await RocketDAOSecurityProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getTotal(), rocketDAOProposal.getState(_proposalID), rocketDAOProposal.getVotesFor(_proposalID), rocketDAOProposal.getVotesRequired(_proposalID), ]).then( ([proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired]) => ({ proposalTotal, proposalState, proposalVotesFor, proposalVotesRequired }), ); } // Add a new proposal await rocketDAOSecurityProposals.connect(txOptions.from).vote(_proposalID, _vote, txOptions); // Capture data let ds2 = await getTxData(); // Check proposals if (ds2.proposalState === proposalStates.Active) { assertBN.isBelow(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is active, votes for proposal should be less than the votes required'); } if (ds2.proposalState === proposalStates.Succeeded) { assertBN.isAtLeast(ds2.proposalVotesFor, ds2.proposalVotesRequired, 'Proposal state is successful, yet does not have the votes required'); } } // Execute a successful proposal export async function daoSecurityExecute(_proposalID, txOptions) { // Load contracts const rocketDAOProposal = await RocketDAOProposal.deployed(); const rocketDAOSecurityProposals = await RocketDAOSecurityProposals.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOProposal.getState(_proposalID), ]).then( ([proposalState]) => ({ proposalState }), ); } // Execute a proposal await rocketDAOSecurityProposals.connect(txOptions.from).execute(_proposalID, txOptions); // Capture data let ds2 = await getTxData(); // Check it was updated assertBN.equal(ds2.proposalState, proposalStates.Executed, 'Proposal is not in the executed state'); } // Join the DAO after a successful invite proposal has passed export async function daoSecurityMemberJoin(txOptions) { // Load contracts const rocketDAOSecurity = await RocketDAOSecurity.deployed(); const rocketDAOSecurityActions = (await RocketDAOSecurityActions.deployed()).connect(txOptions.from); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOSecurity.getMemberCount(), ]).then( ([memberTotal]) => ({ memberTotal }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAOSecurityActions.actionJoin(txOptions); // Capture data let ds2 = await getTxData(); // Check member count has increased assertBN.equal(ds2.memberTotal, ds1.memberTotal + 1n, 'Member count has not increased'); } // Leave the DAO after a successful leave proposal has passed export async function daoSecurityMemberLeave(txOptions) { // Load contracts const rocketDAOSecurity = await RocketDAOSecurity.deployed(); const rocketDAOSecurityActions = await RocketDAOSecurityActions.deployed(); // Get data about the tx function getTxData() { return Promise.all([ rocketDAOSecurity.getMemberCount(), ]).then( ([memberTotal]) => ({ memberTotal }), ); } // Capture data let ds1 = await getTxData(); // Add a new proposal await rocketDAOSecurityActions.connect(txOptions.from).actionLeave(txOptions); // Capture data let ds2 = await getTxData(); // Verify assertBN.equal(ds2.memberTotal, ds1.memberTotal - 1n, 'Member count has not decreased'); } // Request leaving the security council export async function daoSecurityMemberRequestLeave(txOptions) { const rocketDAOSecurityActions = await RocketDAOSecurityActions.deployed(); await rocketDAOSecurityActions.connect(txOptions.from).actionRequestLeave(txOptions); } ================================================ FILE: test/deposit/deposit-pool-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { userDeposit } from '../_helpers/deposit'; import { getMinipoolMinimumRPLStake } from '../_helpers/minipool'; import { submitBalances } from '../_helpers/network'; import { nodeStakeRPL, registerNode, setNodeTrusted } from '../_helpers/node'; import { getRethExchangeRate, getRethTotalSupply, mintRPL } from '../_helpers/tokens'; import { getDepositSetting } from '../_helpers/settings'; import { deposit } from './scenario-deposit'; import { RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsNetwork, RocketDepositPool, } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { assignDeposits } from './scenario-assign-deposits'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; import { nodeDeposit } from '../_helpers/megapool'; const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('Deposit Pool', () => { let owner, node, trustedNode, staker, random; before(async () => { await globalSnapShot(); [ owner, node, trustedNode, staker, random, ] = await ethers.getSigners(); // Register node await registerNode({ from: node }); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); }); // // Deposit // it(printTitle('staker', 'can make a deposit'), async () => { // Set target collateral rate to 0% to avoid deposits sending ETH to rETH instead of deposit pool await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '0'.ether, { from: owner }); // Deposit await deposit({ from: staker, value: '10'.ether, }); // Get current rETH exchange rate let exchangeRate1 = await getRethExchangeRate(); // Update network ETH total to 130% to alter rETH exchange rate let totalBalance = '13'.ether; let rethSupply = await getRethTotalSupply(); let slotTimestamp = '1600000000'; await submitBalances(1, slotTimestamp, totalBalance, 0, rethSupply, { from: trustedNode }); // Get & check updated rETH exchange rate let exchangeRate2 = await getRethExchangeRate(); assertBN.notEqual(exchangeRate1, exchangeRate2, 'rETH exchange rate has not changed'); // Deposit again with updated rETH exchange rate await deposit({ from: staker, value: '10'.ether, }); }); it(printTitle('staker', 'cannot make a deposit while deposits are disabled'), async () => { // Disable deposits await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.enabled', false, { from: owner }); // Attempt deposit await shouldRevert(deposit({ from: staker, value: '10'.ether, }), 'Made a deposit while deposits are disabled'); }); it(printTitle('staker', 'cannot make a deposit below the minimum deposit amount'), async () => { // Get & check deposit amount let minimumDeposit = await getDepositSetting('MinimumDeposit'); let depositAmount = minimumDeposit / 2n; assertBN.isBelow(depositAmount, minimumDeposit, 'Deposit amount is not less than the minimum deposit'); // Attempt deposit await shouldRevert(deposit({ from: staker, value: depositAmount, }), 'Made a deposit below the minimum deposit amount'); }); it(printTitle('staker', 'cannot make a deposit which would exceed the maximum deposit pool size'), async () => { // Set max deposit pool size await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '100'.ether, { from: owner }); // Attempt deposit await shouldRevert(deposit({ from: staker, value: '101'.ether, }), 'Made a deposit which exceeds the maximum deposit pool size'); }); it(printTitle('staker', 'can make a deposit which exceeds the maximum deposit pool if minipool queue is larger'), async () => { // Set max deposit pool size to 1 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '1'.ether, { from: owner }); // Disable socialised assignments so the deposit pool balance check succeeds await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.socialised.maximum', 0, { from: owner }); // Attempt deposit greater than maximum (fails) await shouldRevert(deposit({ from: staker, value: '16'.ether, }), 'Made a deposit which exceeds the maximum deposit pool size'); // Perform 4 node deposits so there is 16 ETH space available for user deposits for (let i = 0; i < 4; ++i) { await nodeDeposit(node); } // Attempt deposit await deposit({ from: staker, value: '16'.ether, }); }); // // Assign deposits // it(printTitle('random address', 'can assign deposits'), async () => { // Assign deposits with no assignable deposits await assignDeposits(1n, { from: staker, }); // Disable deposit assignment await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Deposit and queue up some validators await userDeposit({ from: staker, value: '100'.ether }); for (let i = 0; i < 3; ++i) { await nodeDeposit(node); } // Re-enable deposit assignment & set limit await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.maximum', 3, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.socialised.maximum', 3, { from: owner }); // Assign deposits with assignable deposits await assignDeposits(1n, { from: staker, }); }); it(printTitle('random address', 'cannot assign deposits while deposit assignment is disabled'), async () => { // Disable deposit assignment await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Attempt to assign deposits await shouldRevert(assignDeposits(1n, { from: staker, }), 'Assigned deposits while deposit assignment is disabled'); }); // // Assign deposits // it(printTitle('random address', 'can check maximum deposit amount'), async () => { const rocketDepositPool = await RocketDepositPool.deployed(); // Disable deposits await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.enabled', false, { from: owner }); assertBN.equal(await rocketDepositPool.getMaximumDepositAmount(), 0, 'Invalid maximum deposit amount'); // Enable deposits await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.enabled', true, { from: owner }); const depositPoolMaximum = '100'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', depositPoolMaximum, { from: owner }); assertBN.equal(await rocketDepositPool.getMaximumDepositAmount(), depositPoolMaximum, 'Invalid maximum deposit amount'); // Disable assignments await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Create 4 validators totally 112 ETH extra capacity for (let i = 0; i < 4; ++i) { await nodeDeposit(node); } // Enable assignments to make that extra capacity usable await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); // Check that maximum is correct assertBN.equal(await rocketDepositPool.getMaximumDepositAmount(), depositPoolMaximum + '112'.ether, 'Invalid maximum deposit amount'); }); }); } ================================================ FILE: test/deposit/scenario-assign-deposits.js ================================================ import { RocketDepositPool } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Assign deposits to minipools export async function assignDeposits(max = 1n, txOptions) { const [ rocketDepositPool, ] = await Promise.all([ RocketDepositPool.deployed(), ]); const queueLengthBefore = await rocketDepositPool.getTotalQueueLength(); await rocketDepositPool.connect(txOptions.from).assignDeposits(max); const queueLengthAfter = await rocketDepositPool.getTotalQueueLength(); if (queueLengthBefore <= max) { assertBN.equal(queueLengthAfter, 0n); } else { const queueLengthDelta = queueLengthAfter - queueLengthBefore; assertBN.equal(queueLengthDelta, -max); } } ================================================ FILE: test/deposit/scenario-deposit.js ================================================ import { RocketDAOProtocolSettingsDeposit, RocketDepositPool, RocketTokenRETH, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Make a deposit into the deposit pool export async function deposit(txOptions) { // Load contracts const [ rocketDAOProtocolSettingsDeposit, rocketDepositPool, rocketTokenRETH, rocketVault, ] = await Promise.all([ RocketDAOProtocolSettingsDeposit.deployed(), RocketDepositPool.deployed(), RocketTokenRETH.deployed(), RocketVault.deployed(), ]); // Get parameters let depositFeePerc = await rocketDAOProtocolSettingsDeposit.getDepositFee(); // Get balances function getBalances() { return Promise.all([ rocketDepositPool.getBalance(), rocketDepositPool.getNodeBalance(), ethers.provider.getBalance(rocketVault.target), rocketTokenRETH.balanceOf(txOptions.from), ]).then( ([depositPoolEth, depositPoolNodeEth, vaultEth, userReth]) => ({depositPoolEth, depositPoolNodeEth, vaultEth, userReth}) ); } // Get initial balances let balances1 = await getBalances(); // Deposit await rocketDepositPool.connect(txOptions.from).deposit(txOptions); // Get updated balances let balances2 = await getBalances(); // Calculate values let txValue = BigInt(txOptions.value); let calcBase = '1'.ether; let depositFee = txValue * depositFeePerc / calcBase; let expectedRethMinted = await rocketTokenRETH.getRethValue(txValue - depositFee); // Check balances assertBN.equal(balances2.depositPoolEth, balances1.depositPoolEth + txValue, 'Incorrect updated deposit pool ETH balance'); assertBN.equal(balances2.vaultEth, balances1.vaultEth + txValue, 'Incorrect updated vault ETH balance'); assertBN.equal(balances2.userReth, balances1.userReth + expectedRethMinted, 'Incorrect updated user rETH balance'); } ================================================ FILE: test/megapool/megapool-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { nodeDepositEthFor, registerNode, setNodeTrusted, setNodeWithdrawalAddress } from '../_helpers/node'; import { globalSnapShot, snapshotDescribe } from '../_utils/snapshotting'; import { userDeposit } from '../_helpers/deposit'; import { calculatePositionInQueue, deployMegapool, getMegapoolForNode, getMegapoolWithdrawalCredentials, getValidatorInfo, nodeDeposit, nodeDepositMulti, } from '../_helpers/megapool'; import { shouldRevert } from '../_utils/testing'; import { BeaconStateVerifier, MegapoolUpgradeHelper, RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsMegapool, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsNode, RocketDepositPool, RocketMegapoolDelegate, RocketMegapoolFactory, RocketMegapoolManager, RocketNetworkRevenues, RocketNodeDeposit, RocketStorage, RocketTokenRETH, RocketVault, StorageHelper, } from '../_utils/artifacts'; import assert from 'assert'; import { stakeMegapoolValidator } from './scenario-stake'; import { assertBN } from '../_helpers/bn'; import { exitQueue } from './scenario-exit-queue'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { distributeMegapool } from './scenario-distribute'; import { withdrawCredit } from './scenario-withdraw-credit'; import { notifyExitValidator, notifyFinalBalanceValidator } from './scenario-exit'; import { votePenalty } from './scenario-apply-penalty'; import { reduceBond } from './scenario-reduce-bond'; import { dissolveValidator } from './scenario-dissolve'; import { challengeValidator } from './scenario-challenge'; import { repayDebt } from './scenario-repay-debt'; import { getDepositDataRoot, getValidatorPubkey, getValidatorSignature } from '../_utils/beacon'; import { beaconGenesisTime } from '../_helpers/beaconchain'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; const farFutureEpoch = 2n ** 64n - 1n; async function getCurrentTime() { const latestBlock = await ethers.provider.getBlock('latest'); return latestBlock.timestamp; } async function getValidPrestakeValidator(megapool, index) { const info = await getValidatorInfo(megapool, index); const withdrawalCredentials = await megapool.getWithdrawalCredentials(); return { pubkey: info.pubkey, withdrawalCredentials: withdrawalCredentials, effectiveBalance: '1'.ether / '1'.gwei, slashed: false, activationEligibilityEpoch: farFutureEpoch, activationEpoch: farFutureEpoch, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }; } async function mockExpressTickets(nodeAddress, count) { const helper = await StorageHelper.deployed(); const key = ethers.solidityPackedKeccak256(['string', 'address'], ['node.express.tickets', nodeAddress]); await helper.setUint(key, count); } export default function() { describe('Megapools', () => { let owner, node, node2, nodeWithdrawalAddress, random, trustedNode1, trustedNode2, trustedNode3; let megapool = null; const secondsPerSlot = 12; const slotsPerEpoch = 32; const userDistributeTime = 1575; // Approx 7 days @ 12s/block const userDistributeShortfallTime = 6750; // Approx 30 days @ 12s/block async function mockRewards(megapool, amount = '1'.ether) { await owner.sendTransaction({ to: megapool.target, value: amount, }); } async function getSlotForBlock(blockNumber = null) { const latestBlock = await ethers.provider.getBlock(blockNumber || 'latest'); const currentTime = latestBlock.timestamp; return Math.floor((currentTime - beaconGenesisTime) / secondsPerSlot); } async function getCurrentEpoch() { const slotsPassed = await getSlotForBlock('latest'); return Math.floor(slotsPassed / slotsPerEpoch); } async function waitEpochs(count) { const seconds = count * slotsPerEpoch * secondsPerSlot; await helpers.time.increase(seconds); } async function exitValidator(megapool, index, finalBalance) { const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, index, currentEpoch + 114); await helpers.time.increase(12 * 32 * 114); await notifyFinalBalanceValidator(megapool, index, finalBalance, owner, await getCurrentEpoch() * 32); } before(async () => { await globalSnapShot(); [ owner, node, node2, nodeWithdrawalAddress, random, trustedNode1, trustedNode2, trustedNode3, ] = await ethers.getSigners(); // Register node & set withdrawal address await registerNode({ from: node }); await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, { from: node }); await registerNode({ from: node2 }); // Setup oDAO await registerNode({ from: trustedNode1 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node1@home.com', owner); await registerNode({ from: trustedNode2 }); await setNodeTrusted(trustedNode2, 'saas_2', 'node2@home.com', owner); await registerNode({ from: trustedNode3 }); await setNodeTrusted(trustedNode3, 'saas_3', 'node3@home.com', owner); megapool = await getMegapoolForNode(node); // Disable proof verification const beaconStateVerifier = await BeaconStateVerifier.deployed(); await beaconStateVerifier.setDisabled(true); // Set params await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'user.distribute.window.length', userDistributeTime, { from: owner }); }); // // Factory // it(printTitle('owner', 'can not initialise megapool factory again'), async () => { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); await shouldRevert(rocketMegapoolFactory.initialise(), 'Was able to initialise factory', 'Invalid or outdated network contract'); }); // // General // it(printTitle('node', 'can not upgrade to current delegate'), async () => { await deployMegapool({ from: node }); await shouldRevert(megapool.delegateUpgrade(), 'Was able to upgrade delegate', 'Already using latest'); }); it(printTitle('random', 'can not manually deploy a megapool'), async () => { await shouldRevert(deployMegapool({ from: random }), 'Deployed megapool', 'Invalid node'); }); it(printTitle('node', 'can manually deploy a megapool then deposit'), async () => { await deployMegapool({ from: node }); await nodeDeposit(node); }); it(printTitle('node', 'can manually deploy a megapool then deposit multi'), async () => { await deployMegapool({ from: node }); const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await nodeDepositMulti(node, deposits); }); it(printTitle('node', 'can not deposit multi with excess msg.value'), async () => { const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await shouldRevert( nodeDepositMulti(node, deposits, 0n, '8.1'.ether), 'Deposited multiple with excess msg.value', 'Excess bond value supplied', ); }); it(printTitle('node', 'can not perform multi deposit with no deposits'), async () => { await deployMegapool({ from: node }); const deposits = []; await shouldRevert( nodeDepositMulti(node, deposits), 'Was able to multi deposit with no deposits', 'Must perform at least 1 deposit', ); }); it(printTitle('node', 'can deposit multi with supplied ETH'), async () => { await nodeDepositEthFor(node, { from: random, value: '4'.ether }); const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await nodeDepositMulti(node, deposits, '4'.ether); }); it(printTitle('node', 'can not deposit multi with more credit than exists'), async () => { const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await shouldRevert( nodeDepositMulti(node, deposits, '4'.ether), 'Was able to use more credit than exists', 'Insufficient credit', ); }); it(printTitle('node', 'can not deposit multi with incorrect bond'), async () => { await deployMegapool({ from: node }); const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '2'.ether, useExpressTicket: false, }, ]; await shouldRevert( nodeDepositMulti(node, deposits), 'Was able to deposit with incorrect bond', 'Bond requirement not met', ); }); it(printTitle('node', 'can deposit multi with mixed express ticket usage'), async () => { await deployMegapool({ from: node }); await mockExpressTickets(node.address, 2) const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: true, }, { bondAmount: '4'.ether, useExpressTicket: true, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await nodeDepositMulti(node, deposits); }); it(printTitle('node', 'can deposit multi with mixed bond amounts'), async () => { await deployMegapool({ from: node }); await mockExpressTickets(node.address, 2) await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: true, }, { bondAmount: '2'.ether, useExpressTicket: true, }, { bondAmount: '2'.ether, useExpressTicket: false, }, ]; await nodeDepositMulti(node, deposits); }); it(printTitle('node', 'can exit the queue after a bond reduction and then use credit to deposit'), async () => { const rocketNodeDeposit = await RocketNodeDeposit.deployed(); // Deposit enough for 3 validators await userDeposit({ from: random, value: ('32'.ether - '4'.ether) * 3n }); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); // 4th validator enters the queue assertBN.equal(await megapool.getActiveValidatorCount(), 4n); assertBN.equal(await megapool.getNodeQueuedBond(), '4'.ether); assertBN.equal(await megapool.getNodeBond(), '4'.ether * 3n); // NO has 4 validators with a required 16 ETH bond 1 of those validators is in the queue // Reduce 'reduced.bond' to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Exit the queue await exitQueue(node, 3); // NO should receive 4 ETH credit assertBN.equal(await rocketNodeDeposit.getNodeDepositCredit(node.address), '4'.ether); // NO is over bonded, so should not be able to create a new validator with 2 ETH bond await shouldRevert( nodeDeposit(node, '2'.ether, false, '2'.ether), 'Was able to increase bond while over bonded', 'Bond requirement not met', ); // NO is overbonded by 2 ETH, so should only be able to make validators with 1 ETH bond (prestake value) await nodeDeposit(node, '1'.ether, false, '1'.ether); await nodeDeposit(node, '1'.ether, false, '1'.ether); // NO is now at bond requirement, perform a new deposit with credit at the reduced bond to use up the credit await nodeDeposit(node, '2'.ether, false, '2'.ether); // Used up all credit, should fail to use credit now await shouldRevert(nodeDeposit(node, '2'.ether, false, '2'.ether), 'Exceeded credit', 'Insufficient credit'); }); it(printTitle('node', 'can deposit using supplied ETH'), async () => { await nodeDepositEthFor(node, { from: random, value: '4'.ether }); await nodeDeposit(node, '4'.ether, false, '4'.ether); }); it(printTitle('node', 'can not reuse pubkey'), async () => { // Construct deposit data for prestake let withdrawalCredentials = await getMegapoolWithdrawalCredentials(node.address); let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); const rocketNodeDeposit = await RocketNodeDeposit.deployed(); // Perform first deposit await rocketNodeDeposit.connect(node).deposit('4'.ether, false, depositData.pubkey, depositData.signature, depositDataRoot, { value: '4'.ether }); // Try to deposit again with the same pubkey await shouldRevert( rocketNodeDeposit.connect(node).deposit('4'.ether, false, depositData.pubkey, depositData.signature, depositDataRoot, { value: '4'.ether }), 'Was able to reuse existing pubkey', 'Pubkey in use', ); }); it(printTitle('node', 'can deposit using ETH credit'), async () => { // Enter and exit queue to receive a 4 ETH credit await nodeDeposit(node, '4'.ether); await exitQueue(node, 0); // Use credit on entering the queue again await nodeDeposit(node, '4'.ether, false, '4'.ether); }); it(printTitle('node', 'can deposit multi using ETH credit'), async () => { // Enter and exit queue to receive a 4 ETH credit await nodeDeposit(node, '4'.ether); await exitQueue(node, 0); // Use credit on entering the queue again const deposits = [ { bondAmount: '4'.ether, useExpressTicket: false, }, { bondAmount: '4'.ether, useExpressTicket: false, }, ]; await nodeDepositMulti(node, deposits, '4'.ether); }); it(printTitle('node', 'can not deploy megapool twice'), async () => { await deployMegapool({ from: node }); await shouldRevert(deployMegapool({ from: node }), 'Redeploy worked'); }); it(printTitle('node', 'can not distribute before a validator is created'), async () => { await deployMegapool({ from: node }); megapool = await getMegapoolForNode(node); await shouldRevert( megapool.distribute(), 'Was able to distribute', 'No first validator', ); }); it(printTitle('node', 'can exit the deposit queue and withdraw credit as rETH'), async () => { await deployMegapool({ from: node }); await nodeDeposit(node); await exitQueue(node, 0); // Withdraw 1 ETH worth of rETH await withdrawCredit(node, '1'.ether); // Fail to withdraw 4 ETH worth of rETH await shouldRevert(withdrawCredit(node, '4'.ether), 'Withdrew more rETH than credit', 'Amount exceeds credit available'); }); it(printTitle('node', 'can not withdraw credit as rETH with debt'), async () => { // Enter and exit queue to get a credit await nodeDeposit(node); await exitQueue(node, 0); // Enter queue and perform user deposit to assign await nodeDeposit(node); await userDeposit({ from: random, value: '32'.ether }); // Stake validator await stakeMegapoolValidator(megapool, 1); // Exit with a loss to incur a debt await exitValidator(megapool, 1, '32'.ether - '5'.ether); const nodeDebt = await megapool.getDebt(); assertBN.equal(nodeDebt, '1'.ether); // Fail to withdraw await shouldRevert( withdrawCredit(node, '1'.ether), 'Was able to withdraw credit with debt', 'Cannot withdraw credit while debt exists', ); }); it(printTitle('node', 'can not exit queue twice'), async () => { await deployMegapool({ from: node }); await nodeDeposit(node); await exitQueue(node, 0); await shouldRevert(exitQueue(node, 0), 'Exit queue twice', 'Validator must be in queue'); }); it(printTitle('node', 'can queue up and exit multiple validators'), async () => { const rocketDepositPool = await RocketDepositPool.deployed(); const numValidators = 5; await deployMegapool({ from: node }); for (let i = 0; i < numValidators; i++) { await nodeDeposit(node); const position = await calculatePositionInQueue(megapool, i); assertBN.equal(position, BigInt(i)); } // Check queue top is correct const queueTop = await rocketDepositPool.getQueueTop(); assertBN.equal(queueTop[2], BigInt(await ethers.provider.getBlockNumber() - numValidators + 1)); for (let i = 0; i < numValidators; i++) { await exitQueue(node, i); // Check queue top const queueTop = await rocketDepositPool.getQueueTop(); if (queueTop[1]) { assertBN.equal(queueTop[2], BigInt(await ethers.provider.getBlockNumber())); } } }); it(printTitle('misc', 'calculates position in queue correctly'), async () => { const rocketDepositPool = await RocketDepositPool.deployed(); await mockExpressTickets(node.address, 2) await mockExpressTickets(node2.address, 2) /** * We will add 5 validators to the queue, 2 of which using an express ticket. * * The queue should end up looking like this: * * 0: node-1 (express) * 1: node-2 (express) * 2: node-0 * 3: node-3 * 4: node-4 */ await nodeDeposit(node); // 0 await nodeDeposit(node, '4'.ether, true); // 1 (express) await nodeDeposit(node, '4'.ether, true); // 2 (express) await nodeDeposit(node); // 3 await nodeDeposit(node); // 4 assertBN.equal(await calculatePositionInQueue(megapool, 1n), 0n); assertBN.equal(await calculatePositionInQueue(megapool, 2n), 1n); assertBN.equal(await calculatePositionInQueue(megapool, 0n), 2n); assertBN.equal(await calculatePositionInQueue(megapool, 3n), 3n); assertBN.equal(await calculatePositionInQueue(megapool, 4n), 4n); // Assign one of the validators and re-check positions await userDeposit({ from: random, value: '32'.ether }); // Validator at the top of the queue should be assigned now (in prestake) const info = await getValidatorInfo(megapool, 1n); assert.equal(info.inPrestake, true); /** * Queue should now look like: * * 0: node-2 (express) * 1: node-0 * 2: node-3 * 3: node-4 */ // Should not be in the queue anymore assert.equal(await calculatePositionInQueue(megapool, 1n), null); assertBN.equal(await calculatePositionInQueue(megapool, 2n), 0n); assertBN.equal(await calculatePositionInQueue(megapool, 0n), 1n); assertBN.equal(await calculatePositionInQueue(megapool, 3n), 2n); assertBN.equal(await calculatePositionInQueue(megapool, 4n), 3n); // Prevent new node deposits from assigning and messing up the queue for our test await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Add 2 more validators in the express queue await nodeDeposit(node2, '4'.ether, true); await nodeDeposit(node2, '4'.ether, true); const megapool2 = await getMegapoolForNode(node2); /** * As we just assigned a validator in the express queue, so there should be another express queue on top * followed by a standard, then the 2 new express queue validators * * Therefore, the queue should now look like: * * 0: node-2 (express) * 1: node2-0 (express) * 2: node2-1 (express) * 3: node-0 * 4: node-3 * 5: node-4 */ assertBN.equal(await calculatePositionInQueue(megapool, 2n), 0n); assertBN.equal(await calculatePositionInQueue(megapool2, 0n), 1n); assertBN.equal(await calculatePositionInQueue(megapool2, 1n), 2n); assertBN.equal(await calculatePositionInQueue(megapool, 0n), 3n); assertBN.equal(await calculatePositionInQueue(megapool, 3n), 4n); assertBN.equal(await calculatePositionInQueue(megapool, 4n), 5n); // Assign another validator await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); await userDeposit({ from: random, value: '32'.ether }); /** * The queue should now look like: * * 0: node2-0 (express) * 1: node2-1 (express) * 2: node-0 * 3: node-3 * 4: node-4 */ assertBN.equal(await calculatePositionInQueue(megapool2, 0n), 0n); assertBN.equal(await calculatePositionInQueue(megapool2, 1n), 1n); assertBN.equal(await calculatePositionInQueue(megapool, 0n), 2n); assertBN.equal(await calculatePositionInQueue(megapool, 3n), 3n); assertBN.equal(await calculatePositionInQueue(megapool, 4n), 4n); }); // // Trusted nodes // it(printTitle('trusted node', 'can apply a penalty to a megapool'), async () => { await deployMegapool({ from: node }); await votePenalty(megapool, 0n, '1'.ether, trustedNode1); await votePenalty(megapool, 0n, '1'.ether, trustedNode2); await shouldRevert(votePenalty(megapool, 0n, '1'.ether, trustedNode3), 'Applied penalty past majority', 'Penalty already applied'); }); it(printTitle('trusted node', 'can not apply penalty greater than max'), async () => { const maxPenaltyAmount = '2500'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'maximum.megapool.eth.penalty', maxPenaltyAmount, { from: owner }); await deployMegapool({ from: node }); // Apply a penalty of 1250 await votePenalty(megapool, 0n, maxPenaltyAmount / 2n, trustedNode1); await votePenalty(megapool, 0n, maxPenaltyAmount / 2n, trustedNode2); // Try to apply a penalty of 1251 to exceed the maximum await votePenalty(megapool, 1n, maxPenaltyAmount / 2n + 1n, trustedNode1); await shouldRevert( votePenalty(megapool, 1n, maxPenaltyAmount / 2n + 1n, trustedNode2), 'Was able to exceed maximum', 'Max penalty exceeded', ); }); it(printTitle('trusted node', 'can apply another penalty only after 7 days'), async () => { const maxPenaltyAmount = '2500'.ether; await helpers.mine(); const startTime = await helpers.time.latest(); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'maximum.megapool.eth.penalty', maxPenaltyAmount, { from: owner }); await deployMegapool({ from: node }); await votePenalty(megapool, 0n, '2500'.ether, trustedNode1); await votePenalty(megapool, 0n, '2500'.ether, trustedNode2); const megapoolDebtBefore = await megapool.getDebt(); await helpers.time.increaseTo(startTime + (60 * 60 * 24 * 7) - 10); await votePenalty(megapool, 1n, '2500'.ether, trustedNode1); await shouldRevert(votePenalty(megapool, 1n, '2500'.ether, trustedNode2), 'Applied greater penalty', 'Max penalty exceeded'); await helpers.time.increase(20); await votePenalty(megapool, 1n, '2500'.ether, trustedNode2); const megapoolDebtAfter = await megapool.getDebt(); const debtDelta = megapoolDebtAfter - megapoolDebtBefore; assertBN.equal(debtDelta, '2500'.ether); }); it(printTitle('trusted node', 'can not vote for a penalty greater than maximum'), async () => { const maxPenaltyAmount = '2500'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'maximum.megapool.eth.penalty', maxPenaltyAmount, { from: owner }); await deployMegapool({ from: node }); await shouldRevert( votePenalty(megapool, 0n, maxPenaltyAmount + 1n, trustedNode1), 'Was able to vote for a penalty greater than maximum', 'Penalty exceeds maximum', ); }); it(printTitle('misc', 'should calculate rewards on an empty megapool'), async () => { await deployMegapool({ from: node }); { const rewards = await megapool.calculatePendingRewards(); assertBN.equal(rewards[0], 0n); assertBN.equal(rewards[1], 0n); assertBN.equal(rewards[2], 0n); } await mockRewards(megapool, '1'.ether); { const rewards = await megapool.calculatePendingRewards(); assertBN.equal(rewards[0], '1'.ether); assertBN.equal(rewards[1], 0n); assertBN.equal(rewards[2], 0n); } }); snapshotDescribe('With challenged megapool', () => { let rocketMegapoolManager; before(async () => { // Deposit enough for 3 validators await userDeposit({ from: random, value: ('32'.ether - '3'.ether) * 4n }); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); // Last validator will not be "staking" await stakeMegapoolValidator(megapool, 0); await stakeMegapoolValidator(megapool, 1); await stakeMegapoolValidator(megapool, 2); // rocketMegapoolManager = await RocketMegapoolManager.deployed(); const tx = await rocketMegapoolManager.connect(trustedNode1).challengeExit([ { megapool: megapool.target, validatorIds: [ 0, ], }, ]); const challengeSlot = await getSlotForBlock(tx.blockNumber); // Mock some rewards await mockRewards(megapool, '1'.ether); const info = await getValidatorInfo(megapool, 0); assert.equal(info.locked, true); assert(info.lockedSlot >= challengeSlot); }); it(printTitle('random', 'can not challenge a megapool'), async () => { await shouldRevert( challengeValidator(megapool, [1n], random), 'Was able to challenge', 'Invalid trusted node', ); }); it(printTitle('node', 'can not challenge a megapool'), async () => { await shouldRevert( challengeValidator(megapool, [1n], node), 'Was able to challenge', 'Invalid trusted node', ); }); it(printTitle('trusted node', 'can not challenge twice in a row'), async () => { await shouldRevert( challengeValidator(megapool, [0n], trustedNode1), 'Was able to challenge again', 'Member was last to challenge', ); }); it(printTitle('trusted node', 'can update challenge to newer slot'), async () => { const infoBefore = await getValidatorInfo(megapool, 0); await helpers.time.increase(secondsPerSlot * 2); await challengeValidator(megapool, [0n], trustedNode2); const infoAfter = await getValidatorInfo(megapool, 0); assert(infoAfter.lockedSlot > infoBefore.lockedSlot); }); it(printTitle('trusted node', 'can challenge if was not last to challenge'), async () => { await challengeValidator(megapool, [0n], trustedNode2); }); it(printTitle('trusted node', 'can challenge multiple'), async () => { await challengeValidator(megapool, [0n, 1n, 2n], trustedNode2); }); it(printTitle('trusted node', 'can not challenge a non-staking validator'), async () => { await shouldRevert( challengeValidator(megapool, [3n], trustedNode2), 'Was able to challenge', 'Validator not staked', ); const withdrawalEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 2n, withdrawalEpoch); await shouldRevert( challengeValidator(megapool, [2n], trustedNode2), 'Was able to challenge', 'Already exiting', ); await notifyFinalBalanceValidator(megapool, 2n, '32'.ether, owner, withdrawalEpoch * 32); await shouldRevert( challengeValidator(megapool, [2n], trustedNode2), 'Was able to challenge', 'Already exited', ); }); it(printTitle('node', 'can not distribute while challenged'), async () => { await shouldRevert( distributeMegapool(megapool), 'Was able to distribute while locked', 'Megapool locked', ); }); it(printTitle('node', 'can not prove not exiting on a non-locked validator'), async () => { const info = await getValidatorInfo(megapool, 1n); const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const validValidator = { pubkey: info.pubkey, withdrawalCredentials: withdrawalCredentials, effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: farFutureEpoch, }; const validSlot = BigInt((await getCurrentEpoch() + 1) * 32); const validProof = { validatorIndex: 0n, validator: validValidator, witnesses: [], }; const slotProof = { slot: validSlot, witnesses: [], }; await shouldRevert( rocketMegapoolManager.notifyNotExit(megapool.target, 1n, await getCurrentTime(), validProof, slotProof), 'Was able to notify not exit on non-locked validator', 'Validator not locked', ); }); it(printTitle('node', 'can prove not exit'), async () => { const info = await getValidatorInfo(megapool, 0n); const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const validValidator = { pubkey: info.pubkey, withdrawalCredentials: withdrawalCredentials, effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: farFutureEpoch, }; const currentTime = await getCurrentTime(); const validProof = { validatorIndex: 0n, validator: validValidator, witnesses: [], }; const tooOldProof = { slot: 1n, validatorIndex: 0n, validator: validValidator, witnesses: [], }; const wrongPubkeyProof = { validatorIndex: 0n, validator: { ...validValidator, pubkey: '0x01', }, witnesses: [], }; const exitingValidatorProof = { validatorIndex: 0n, validator: { ...validValidator, withdrawableEpoch: await getCurrentEpoch(), }, witnesses: [], }; const tooOldSlotProof = { slot: 1n, witnesses: [], }; const slotProof = { slot: currentTime, witnesses: [], }; await shouldRevert( rocketMegapoolManager.notifyNotExit(megapool.target, 0n, currentTime - 60, tooOldProof, tooOldSlotProof), 'Invalid proof accepted', 'Proof is older than challenge', ); await shouldRevert( rocketMegapoolManager.notifyNotExit(megapool.target, 0n, currentTime, wrongPubkeyProof, slotProof), 'Invalid proof accepted', 'Pubkey does not match', ); await shouldRevert( rocketMegapoolManager.notifyNotExit(megapool.target, 0n, currentTime, exitingValidatorProof, slotProof), 'Invalid proof accepted', 'Validator already exiting', ); // Correct proof should work await rocketMegapoolManager.notifyNotExit(megapool.target, 0n, currentTime, validProof, slotProof); const infoAfter = await getValidatorInfo(megapool, 0); assert.equal(infoAfter.locked, false); // Distribution should now work await distributeMegapool(megapool); }); it(printTitle('node', 'can unlock by notifying exit'), async () => { await notifyExitValidator(megapool, 0, await getCurrentEpoch()); const infoAfter = await getValidatorInfo(megapool, 0); assert.equal(infoAfter.locked, false); const lockedCount = await megapool.getLockedValidatorCount(); assertBN.equal(lockedCount, 0n); }); }); it(printTitle('node', 'can not reduce bond with queued validators'), async () => { await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); await shouldRevert( reduceBond(megapool, '2'.ether), 'Was able to reduce bond', 'Cannot reduce bond with queued validators', ); }); it(printTitle('node', 'has bond reduced during dissolve'), async () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); // Deposit 40 ETH await userDeposit({ from: random, value: '4'.ether * 10n }); // Make 24 validators for (let i = 0n; i < 24n; i++) { await nodeDeposit(node); } // Node should now have 4 active and 20 queued validators assertBN.equal(await megapool.getNodeBond(), '16'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '80'.ether); // Reduce bond await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Deposit 32 user ETH to prestake validator 5 and dissolve await userDeposit({ from: random, value: '32'.ether }); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 4n, random); // The active bond is no 0 ETH the bond requirement is being fully met by queued validators assertBN.equal(await megapool.getNodeBond(), '0'.ether); // Dissolve all remaining validators for (let i = 5n; i < 24n; i++) { await userDeposit({ from: random, value: '32'.ether }); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, i, random); } // NO should now have the exact bond required for 4 validators (2 * 4 ETH + 2 * 2 ETH = 12 ETH) assertBN.equal(await megapool.getNodeBond(), '12'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '0'.ether); assertBN.equal(await megapool.getUserCapital(), ('32'.ether * 4n) - '12'.ether); assertBN.equal(await megapool.getUserQueuedCapital(), '0'.ether); // Finish up exiting all remaining validators for (let i = 0n; i < 4n; i++) { await stakeMegapoolValidator(megapool, i); await exitValidator(megapool, i, '32'.ether); } assertBN.equal(await megapool.getNodeBond(), '0'.ether); assertBN.equal(await megapool.getUserCapital(), '0'.ether); }); it(printTitle('node', 'can create a new validator if bond requirement increases'), async () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); // Reduce reduced.bond to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Set penalty to 0.1 ETH const dissolvePenalty = '0.1'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner }); // Deposit 56 ETH await userDeposit({ from: random, value: '20'.ether }); // Make 2 validators with 4 ETH bond for (let i = 0n; i < 2n; i++) { await nodeDeposit(node, '4'.ether); } // Make 2 validators with 2 ETH bond for (let i = 0n; i < 2n; i++) { await nodeDeposit(node, '2'.ether); } // Increase reduced.bond back to 4 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '4'.ether, { from: owner }); // Although bond requirement is now 4 ETH, NO is underbonded due to change in bond await shouldRevert( nodeDeposit(node, '4'.ether), 'Was able to deposit with 4 ETH', 'Bond requirement not met', ); // Bond requirement for 5 validators is now 20 ETH and NO has 12, so 8 ETH is required await nodeDeposit(node, '8'.ether); }); it(printTitle('node', 'can create a new validator if bond requirement increases and node is underbonded by > 32 ETH'), async () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); // Reduce reduced.bond to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Increase deposit pool capacity await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '10000'.ether, { from: owner }); // Set penalty to 0.1 ETH const dissolvePenalty = '0.1'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner }); // Deposit ETH await userDeposit({ from: random, value: '30'.ether * 35n }); // Make 32 validators with 2 ETH bond await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); for (let i = 0n; i < 30n; i++) { await nodeDeposit(node, '2'.ether); } // Node should now have 32 active validators with a bond of 4+4+(2*30) = 68ETH assertBN.equal(await megapool.getNodeBond(), '68'.ether); assertBN.equal(await megapool.getUserCapital(), '32'.ether * 32n - '68'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '0'.ether); assertBN.equal(await megapool.getUserQueuedCapital(), '0'.ether); // Increase reduced.bond back to 4 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '4'.ether, { from: owner }); // Node is now underbonded by 4+4+(4*30)-68 = 60 ETH so next validator must have 32 ETH bond assertBN.equal(await megapool.getNewValidatorBondRequirement(), '32'.ether); await nodeDeposit(node, '32'.ether); // Node now has bond of 68+32 = 100, bond requirement with new validator is is 4+4+(4*32) = 136 ETH, so next validator requires 32 ETH assertBN.equal(await megapool.getNodeBond(), '100'.ether); assertBN.equal(await megapool.getNewValidatorBondRequirement(), '32'.ether); await nodeDeposit(node, '32'.ether); // Node now has bond of 100 + 32 = 132, bond requirement with new validator is 4+4+(4*33) = 140 ETH, so next validator requires 8 ETH assertBN.equal(await megapool.getNodeBond(), '132'.ether); assertBN.equal(await megapool.getNewValidatorBondRequirement(), '8'.ether); await nodeDeposit(node, '8'.ether); }); it(printTitle('node', 'can dissolve and exit validators when underbonded'), async () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); // Reduce reduced.bond to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Set penalty to 0.1 ETH const dissolvePenalty = '0.1'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner }); // Deposit 56 ETH await userDeposit({ from: random, value: '20'.ether }); // Make 2 validators with 4 ETH bond for (let i = 0n; i < 2n; i++) { await nodeDeposit(node, '4'.ether); } // Make 52 validators with 2 ETH bond for (let i = 0n; i < 52n; i++) { await nodeDeposit(node, '2'.ether); } // Node should now have 4 active (4+4+2+2=12ETH) and 50 queued validators assertBN.equal(await megapool.getNodeBond(), '12'.ether); assertBN.equal(await megapool.getUserCapital(), '116'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '100'.ether); assertBN.equal(await megapool.getUserQueuedCapital(), '1500'.ether); // Increase reduced.bond back to 4 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '4'.ether, { from: owner }); // Deposit enough to assign 15 validator await userDeposit({ from: random, value: '32'.ether * 15n }); await helpers.time.increase(dissolvePeriod + 1); /** * The NO now has 19 active validators and 35 queued validators * * Node bond should be 4 ETH for the first 2, and 2 ETH for the remaining 15 = 42 ETH * User capital should be 32 ETH * 19 validators - nodeBond = 566 ETH * Node queued bond should be 2 ETH * 35 = 70 ETH * User queued capital should be 32 ETH * 35 - nodeQueuedBond = 990 ETH */ assertBN.equal(await megapool.getNodeBond(), '42'.ether); assertBN.equal(await megapool.getUserCapital(), '566'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '70'.ether); assertBN.equal(await megapool.getUserQueuedCapital(), '1050'.ether); // Disable assignments to prevent exits from performing assignments await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Dissolve 10 validators for (let i = 4n; i < 14n; i += 1n) { await dissolveValidator(node, i, random); } /** * The NO now has 4 active validators, 10 dissolved validators and 40 queued validators * * For each of the dissolved validators, the NO was unable to cover the lost 1 ETH so they should have * a debt of 10 ETH + 1 ETH in dissolve penalties = 11 ETH * * Node bond should not have changed = 42 ETH * User capital should have decreased by 32 ETH for each dissolved validator = 566 - (32 * 10) = 246 ETH * * Queued bond/capital should not have changed */ assertBN.equal(await megapool.getNodeBond(), '42'.ether); assertBN.equal(await megapool.getUserCapital(), '246'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '70'.ether); assertBN.equal(await megapool.getUserQueuedCapital(), '1050'.ether); assertBN.equal(await megapool.getDebt(), (dissolvePenalty + '1'.ether) * 10n); // Finish up exiting all remaining validators await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '10000'.ether, { from: owner }); // Assign, stake and exit all the queued validators for (let i = 14n; i < 54n; i++) { await userDeposit({ from: random, value: '32'.ether }); await helpers.time.increase(dissolvePeriod + 1); await stakeMegapoolValidator(megapool, i); await exitValidator(megapool, i, '32'.ether); } // Stake and exit the first 4 validators await helpers.time.increase(dissolvePeriod + 1); for (let i = 0n; i < 4n; i++) { await stakeMegapoolValidator(megapool, i); await exitValidator(megapool, i, '32'.ether); } assertBN.equal(await megapool.getNodeBond(), '0'.ether); assertBN.equal(await megapool.getUserCapital(), '0'.ether); }); it(printTitle('node', 'cannot exit queue to underbonded state due to dissolves'), async () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); // Deposit 4 ETH await userDeposit({ from: random, value: '4'.ether * 10n }); // Make 24 validators for (let i = 0n; i < 24n; i++) { await nodeDeposit(node); } // Node should now have 4 active and 20 queued validators assertBN.equal(await megapool.getNodeBond(), '16'.ether); assertBN.equal(await megapool.getNodeQueuedBond(), '80'.ether); // Reduce bond await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Deposit 32 user ETH to prestake validator 5 and dissolve await userDeposit({ from: random, value: '32'.ether }); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 4n, random); // NO's non-queued bond is now 0 ETH - the bond requirement is being fully met by queued validators assertBN.equal(await megapool.getNodeBond(), '0'.ether); // Exit the queue for 13 validators const rocketNodeDeposit = await RocketNodeDeposit.deployed(); for (let i = 5n; i < 18n; i++) { await exitQueue(node, i); } /** * Node operator started with 24 validators * - 4 are active * - 1 was dissolved * - 13 have exited the queue * - 6 remain in the queue * * Bond is 0 ETH * Queued bond is 6 * 4 = 24 ETH * Effective bond is 24 ETH * * Current bond requirement with 10 validators is 2 * 4 ETH + 8 * 2 ETH = 24 ETH * * Exiting the queue now would result in NO being underbonded */ { const bondRequirement = await rocketNodeDeposit.getBondRequirement(await megapool.getActiveValidatorCount()); const nodeBond = await megapool.getNodeBond(); const nodeQueuedBond = await megapool.getNodeQueuedBond(); assertBN.equal(bondRequirement, '24'.ether); assertBN.equal(nodeBond, '0'.ether); assertBN.equal(nodeQueuedBond, '24'.ether); } // NO cannot exit queued await shouldRevert(exitQueue(node, 18n), 'Was able to exit queue', 'Bond requirement not met'); // NO can exit an active validator to reduce bond requirement and then exit queue await stakeMegapoolValidator(megapool, 0); await exitValidator(megapool, 0n, '32'.ether); /* * Now a validator has exited the bond requirement is 2 * 4 ETH + 7 * 2 ETH = 22 ETH * Effective bond is still 24 ETH (queued bond did not change) * NO should now be able to exit a validator from the queue */ { const bondRequirement = await rocketNodeDeposit.getBondRequirement(await megapool.getActiveValidatorCount()); const nodeBond = await megapool.getNodeBond(); const nodeQueuedBond = await megapool.getNodeQueuedBond(); assertBN.equal(bondRequirement, '22'.ether); assertBN.equal(nodeBond, '0'.ether); assertBN.equal(nodeQueuedBond, '24'.ether); } await exitQueue(node, 18n); // Disable assignments to prevent exits from performing assignments await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Finish up by exiting all validators and exiting queue completely for (let i = 1n; i < 4n; i++) { await stakeMegapoolValidator(megapool, i); await exitValidator(megapool, i, '32'.ether); } for (let i = 19n; i < 24n; i++) { await exitQueue(node, i); } // Check state { const bondRequirement = await rocketNodeDeposit.getBondRequirement(await megapool.getActiveValidatorCount()); const nodeBond = await megapool.getNodeBond(); const nodeQueuedBond = await megapool.getNodeQueuedBond(); assertBN.equal(bondRequirement, '0'.ether); assertBN.equal(nodeBond, '0'.ether); assertBN.equal(nodeQueuedBond, '0'.ether); } }); snapshotDescribe('With overbonded megapool', () => { before(async () => { // Deposit enough for 4 validators await userDeposit({ from: random, value: ('32'.ether - '4'.ether) * 4n }); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await nodeDeposit(node, '4'.ether); await stakeMegapoolValidator(megapool, 0); await stakeMegapoolValidator(megapool, 1); await stakeMegapoolValidator(megapool, 2); // Reduce 'reduced.bond' to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Node is now overbonded by 2 ETH }); it(printTitle('node', 'can not reduce bond below requirement'), async () => { await shouldRevert(reduceBond(megapool, '3'.ether), 'Reduced bond below requirement', 'New bond is too low'); }); it(printTitle('node', 'can partially reduce bond'), async () => { await reduceBond(megapool, '1'.ether); }); it(printTitle('node', 'can not reduce bond while at minimum'), async () => { await reduceBond(megapool, '2'.ether); await shouldRevert(reduceBond(megapool, '1'.ether), 'Reduced bond below requirement', 'Bond is at minimum'); }); it(printTitle('node', 'can not reduce bond with debt'), async () => { await votePenalty(megapool, 0n, '1'.ether, trustedNode1); await votePenalty(megapool, 0n, '1'.ether, trustedNode2); await shouldRevert(reduceBond(megapool, '2'.ether), 'Reduced bond with debt', 'Cannot reduce bond with debt'); }); it(printTitle('node', 'can reduce bond to new requirement and use credit for another validator'), async () => { await reduceBond(megapool, '2'.ether); await nodeDeposit(node, '2'.ether, false, '2'.ether); }); it(printTitle('node', 'can reduce bond to new requirement and use credit to mint rETH'), async () => { await reduceBond(megapool, '2'.ether); await withdrawCredit(node, '2'.ether); }); it(printTitle('node', 'can reduce bond to new requirement and use credit to mint rETH from their withdrawal address'), async () => { await reduceBond(megapool, '2'.ether); // Fail to withdraw credit from random address await shouldRevert( withdrawCredit(node, '2'.ether, random), 'Was able to withdraw credit from random address', 'Must be called from withdrawal address', ); // Withdraw credit from withdrawal address await withdrawCredit(node, '2'.ether, nodeWithdrawalAddress); }); it(printTitle('node', 'can reduce bond to new requirement and use some credit for another validator and some for rETH'), async () => { await reduceBond(megapool, '2'.ether); await withdrawCredit(node, '1'.ether); await nodeDeposit(node, '2'.ether, false, '1'.ether); }); }); snapshotDescribe('With full deposit pool', () => { const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days before(async () => { // Deposit ETH into deposit pool await userDeposit({ from: random, value: '32'.ether * 10n }); // Set time before dissolve await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner }); }); it(printTitle('node', 'can deposit while assignments are disabled and be assigned once enabled again'), async () => { const rocketDepositPool = await RocketDepositPool.deployed(); // Disable deposit assignments await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Deploy a validator await deployMegapool({ from: node }); await nodeDeposit(node); const topBefore = await rocketDepositPool.getQueueTop(); assert.equal(topBefore[1], false); // Check the validator is still in the queue const megapool = await getMegapoolForNode(node); const validatorInfoBefore = await megapool.getValidatorInfo(0); assert.equal(validatorInfoBefore.inQueue, true); // Enable assignments await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); // Check queue top is now assignable const topAfter = await rocketDepositPool.getQueueTop(); assert.equal(topAfter[1], true); // Assign the validator from random user await rocketDepositPool.connect(random).assignDeposits(1); // Check the validator is now assigned const validatorInfoAfter = await megapool.getValidatorInfo(0); assert.equal(validatorInfoAfter.inQueue, false); }); it(printTitle('node', 'cannot exit the deposit queue once assigned'), async () => { await deployMegapool({ from: node }); await nodeDeposit(node); await shouldRevert(exitQueue(node, 0), 'Was able to exit the deposit queue once assigned', 'Validator must be in queue'); }); it(printTitle('node', 'can not create a new validator while debt is present'), async () => { await deployMegapool({ from: node }); await votePenalty(megapool, 0n, '1'.ether, trustedNode1); await votePenalty(megapool, 0n, '1'.ether, trustedNode2); await shouldRevert(nodeDeposit(node), 'Created validator', 'Cannot create validator while debt exists'); }); it(printTitle('node', 'can create new validators per bond requirements'), async () => { await mockExpressTickets(node.address, 2) await shouldRevert(nodeDeposit(node, '8'.ether), 'Created validator', 'Bond requirement not met'); await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); await nodeDeposit(node); await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); await nodeDeposit(node); await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); await nodeDeposit(node); await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); }); it(printTitle('node', 'can not consume more than provisioned express tickets'), async () => { await mockExpressTickets(node.address, 2) await nodeDeposit(node, '4'.ether, true); await nodeDeposit(node, '4'.ether, true); await shouldRevert(nodeDeposit(node, '4'.ether, true), 'Consumed express ticket', 'No express tickets'); }); it(printTitle('node', 'can create a new validator without an express ticket'), async () => { await nodeDeposit(node); }); it(printTitle('node', 'can create a new validator with an express ticket'), async () => { await mockExpressTickets(node.address, 1) await nodeDeposit(node, '4'.ether, true); }); it(printTitle('random', 'can not dissolve validator before dissolve period ends'), async () => { await nodeDeposit(node); await shouldRevert(megapool.connect(random).dissolveValidator(0), 'Dissolved validator', 'Not enough time has passed'); }); it(printTitle('random', 'can dissolve validator after dissolve period ends'), async () => { await nodeDeposit(node); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 0, random); }); it(printTitle('node', 'can exit a dissolved validator'), async () => { await nodeDeposit(node); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 0, random); const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 5); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, (currentEpoch + 5) * 32); }); it(printTitle('node', 'can stake after dissolve when dusted'), async () => { // Set penalty to 0.1 ETH const dissolvePenalty = '0.1'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner }); // Dissolve a validator await nodeDeposit(node); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 0n, random); // Pay the penalty debt await repayDebt(megapool, dissolvePenalty); // Dust the megapool await random.sendTransaction({ to: megapool.target, value: '0.01'.ether, }); // Try to staka await nodeDeposit(node); await stakeMegapoolValidator(megapool, 1n); }); it(printTitle('node', 'can not exit a dissolve validator with invalid withdrawalCredentials'), async () => { await nodeDeposit(node); await helpers.time.increase(dissolvePeriod + 1); await dissolveValidator(node, 0, random); const currentEpoch = await getCurrentEpoch(); const info = await getValidatorInfo(megapool, 0n); const invalidValidator = { pubkey: info.pubkey, withdrawalCredentials: '0x0100000000000000000000000000000000000000000000000000000000000000', effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: currentEpoch, }; const currentTime = await getCurrentTime(); const invalidProof = { validatorIndex: 0n, validator: invalidValidator, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await shouldRevert( rocketMegapoolManager.notifyExit(megapool.target, 0n, await getCurrentTime(), invalidProof, slotProof), 'Was able to notify exit on dissolved validator', 'Invalid withdrawal credentials', ); }); it(printTitle('random', 'can dissolve validator immediately with a state proof'), async () => { await nodeDeposit(node); const info = await getValidatorInfo(megapool, 0); const incorrectCredentials = { slot: 0n, validatorIndex: 0n, validator: { pubkey: info.pubkey, withdrawalCredentials: '0x0100000000000000000000000000000000000000000000000000000000000000', effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: 0n, }, witnesses: [], }; await dissolveValidator(node, 0, random, incorrectCredentials); }); it(printTitle('random', 'can not dissolve validator immediately with compliant validator'), async () => { await nodeDeposit(node); const info = await getValidatorInfo(megapool, 0); const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const correctProof = { slot: 0n, validatorIndex: 0n, validator: { pubkey: info.pubkey, withdrawalCredentials: withdrawalCredentials, effectiveBalance: '1'.ether / '1'.gwei, slashed: false, activationEligibilityEpoch: farFutureEpoch, activationEpoch: farFutureEpoch, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: [], }; await shouldRevert( dissolveValidator(node, 0, random, correctProof), 'Was able to dissolve validator', 'Validator is compliant', ); }); it(printTitle('random', 'can not dissolve a validator immediately with non-matching pubkey'), async () => { await nodeDeposit(node); const incorrectPubkey = { validatorIndex: 0n, validator: { pubkey: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', withdrawalCredentials: '0x0100000000000000000000000000000000000000000000000000000000000000', effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: 0n, }, witnesses: [], }; await shouldRevert( dissolveValidator(node, 0, random, incorrectPubkey), 'Was able to dissolve validator', 'Pubkey does not match', ); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid withdrawal_credentials)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), withdrawalCredentials: '0x0100000000000000000000000000000000000000000000000000000000000000', }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (slashed)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), slashed: true, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid withdrawable_epoch)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), withdrawableEpoch: 100n, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid exit_epoch)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), exitEpoch: 100n, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid activation_eligibility_epoch)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), activationEligibilityEpoch: 100n, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid activation_epoch)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), activationEpoch: 100n, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('random', 'can dissolve an invalid validator immediately (invalid balance)'), async () => { await nodeDeposit(node); const invalidProof = { slot: 0n, validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), effectiveBalance: '33'.ether / '1'.gwei, }, witnesses: [], }; await dissolveValidator(node, 0, random, invalidProof); }); it(printTitle('node', 'can perform stake operation on pre-stake validator'), async () => { await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0); }); it(printTitle('random', 'can perform stake operation on pre-stake validator'), async () => { await nodeDeposit(node); const megapoolRandom = megapool.connect(random); await stakeMegapoolValidator(megapoolRandom, 0); }); it(printTitle('node', 'can not stake with invalid validator (invalid withdrawal_credentials)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), withdrawalCredentials: Buffer.from('0100000000000000000000000000000000000000000000000000000000000000', 'hex'), }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Invalid withdrawal credentials'); }); it(printTitle('node', 'can not stake with invalid validator (invalid withdrawable_epoch)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), withdrawableEpoch: 100n, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Validator is withdrawing'); }); it(printTitle('node', 'can not stake with invalid validator (invalid exit_epoch)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), exitEpoch: 100n, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Validator is exiting'); }); it(printTitle('node', 'can not stake with invalid validator (invalid activation_eligibility_epoch)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), activationEligibilityEpoch: 100n, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Validator is activating'); }); it(printTitle('node', 'can not stake with invalid validator (invalid activation_epoch)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), activationEpoch: 100n, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Validator is activated'); }); it(printTitle('node', 'can not stake with invalid validator (invalid slashed)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), slashed: true, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Validator is slashed'); }); it(printTitle('node', 'can not stake with invalid validator (invalid balance)'), async () => { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await nodeDeposit(node); // Construct a fake proof const proof = { validatorIndex: 0n, validator: { ...await getValidPrestakeValidator(megapool, 0n), effectiveBalance: '32'.ether / '1'.gwei, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; await shouldRevert(rocketMegapoolManager.stake(megapool.target, 0n, await getCurrentTime(), proof, slotProof), 'Staked with invalid validator', 'Invalid validator balance'); }); it(printTitle('node', 'can perform a second stake operation with no rewards available'), async () => { await nodeDeposit(node); await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0); await stakeMegapoolValidator(megapool, 1); }); it(printTitle('node', 'can calculate rewards correctly when capital ratio changes over time'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); const rocketMegapoolManager = await RocketMegapoolManager.deployed(); // Adjust reduced.bond to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Create 2 validators with 4 ETH bond each await nodeDeposit(node); await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0); await stakeMegapoolValidator(megapool, 1); // Challenge exit to prevent forced distributions await rocketMegapoolManager.connect(trustedNode1).challengeExit([ { megapool: megapool.target, validatorIds: [ 0, ], }, ]); // Increase time to impact the time-weighted calculations const lastDistributionTime = await megapool.getLastDistributionTime(); await helpers.time.increaseTo(lastDistributionTime + 99n); // Check ratio assertBN.equal(await rocketNetworkRevenues.getNodeCapitalRatio(node.address), '0.125'.ether); // Create 1 more validators with 2 ETH bond await nodeDeposit(node, '2'.ether); await stakeMegapoolValidator(megapool, 2); // Increase time so that we have as much time at the old ratio as we do at the new one await helpers.time.increaseTo(lastDistributionTime + 201n); // Mock rewards await mockRewards(megapool, '1'.ether); const pendingRewards = await megapool.getPendingRewards(); assertBN.equal(pendingRewards, '1'.ether); // Check ratio assertBN.equal(await rocketNetworkRevenues.getNodeCapitalRatio(node.address), '0.10416'.ether); /* Check average ratio over past 200 seconds 100 seconds of 0.125 ratio 100 seconds of 0.10416 ratio ((100 * 0.125) + (100 * 0.10416)) / 200 = 0.11458 */ assertBN.equal(await rocketNetworkRevenues.getNodeAverageCapitalRatioSince(node.address, lastDistributionTime), '0.11458'.ether); /* Rewards: 1 ETH Avg. Collat Ratio: 11.458% Node Portion: 0.1148 ETH User Portion: 0.88542 ETH Commission: 0.88542 * 5% = 0.044271 ETH Node Share: 0.044271 + 0.11459 = 0.158861 ETH Voter Share: 0.88541 * 9% = 0.0796869 ETH rETH Share: 1 - 0.158861 - 0.0796869 = 0.7614521 ETH Note: calculations on-chain are of 3 fixed point precision */ const rewardSplit = await megapool.calculatePendingRewards(); assertBN.almostEqual(rewardSplit[0], '0.158861'.ether, '0.0001'.ether); // Node assertBN.almostEqual(rewardSplit[1], '0.0796869'.ether, '0.0001'.ether); // Voter assertBN.equal(rewardSplit[2], '0'.ether); // pDAO assertBN.almostEqual(rewardSplit[3], '0.7614521'.ether, '0.0001'.ether); // User }); it(printTitle('node', 'can exit all validators then capital ratio is reset when creating a new one'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); const rocketMegapoolManager = await RocketMegapoolManager.deployed(); // Adjust reduced.bond to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Create 3 validators await nodeDeposit(node); await nodeDeposit(node); await nodeDeposit(node, '2'.ether); await stakeMegapoolValidator(megapool, 0); await stakeMegapoolValidator(megapool, 1); await stakeMegapoolValidator(megapool, 2); // Capital ratio should be 10.415% (4+4+2 / 32+32+32) assertBN.equal(await rocketNetworkRevenues.getNodeAverageCapitalRatioSince(node.address, await megapool.getLastDistributionTime()), '0.10416'.ether); // Exit a validator await exitValidator(megapool, 0, '32'.ether); // Capital ratio should be back to 12.5% assertBN.equal(await rocketNetworkRevenues.getNodeAverageCapitalRatioSince(node.address, await megapool.getLastDistributionTime()), '0.125'.ether); // Exit remaining 2 validators await exitValidator(megapool, 1, '32'.ether); await exitValidator(megapool, 2, '32'.ether); // Capital ratio should remain 12.5% assertBN.equal(await rocketNetworkRevenues.getNodeAverageCapitalRatioSince(node.address, await megapool.getLastDistributionTime()), '0.125'.ether); // Create a new validator await nodeDeposit(node); await stakeMegapoolValidator(megapool, 3); // Capital ratio should be back to 12.5% const lastDistributionTime = await megapool.getLastDistributionTime(); assertBN.equal(await rocketNetworkRevenues.getNodeAverageCapitalRatioSince(node.address, lastDistributionTime), '0.125'.ether); // Last distribution time should have been updated to the latest block where stake occurred assertBN.equal(lastDistributionTime, await helpers.time.latest()); }); snapshotDescribe('With staking validator', () => { before(async () => { await deployMegapool({ from: node }); await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0); }); it(printTitle('node', 'cannot perform stake operation on staking validator'), async () => { await shouldRevert(stakeMegapoolValidator(megapool, 0), 'Was able to stake', 'Validator must be pre-staked'); }); it(printTitle('node', 'can distribute rewards'), async () => { await mockRewards(megapool, '1'.ether); const pendingRewards = await megapool.getPendingRewards(); assertBN.equal(pendingRewards, '1'.ether); /* Rewards: 1 ETH Collat Ratio: 1/8 Node Portion: 0.125 ETH User Portion: 0.875 ETH Commission: 0.875 * 5% = 0.04375 ETH Node Share: 0.04375 + 0.125 = 0.16875 ETH Voter Share: 0.875 * 9% = 0.07875 ETH rETH Share: 1 - 0.16875 - 0.07875 = 0.7525 ETH Note: calculations on-chain are of 3 fixed point precision */ const rewardSplit = await megapool.calculatePendingRewards(); assertBN.equal(rewardSplit[0], '0.16875'.ether); // Node assertBN.equal(rewardSplit[1], '0.07875'.ether); // Voter assertBN.equal(rewardSplit[2], '0'.ether); // pDAO assertBN.equal(rewardSplit[3], '0.7525'.ether); // User // Perform distribution await distributeMegapool(megapool); }); it(printTitle('node', 'can distribute to pdao rewards'), async () => { // Set pDAO share to 1% await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.pdao.share', '0.01'.ether, { from: owner }); // Distribute some rewards to reset time-weighted calculations await mockRewards(megapool, '1'.ether); await megapool.distribute(); // Mock 1 ETH of rewards await mockRewards(megapool, '1'.ether); /* Rewards: 1 ETH Collat Ratio: 1/8 Node Portion: 0.125 ETH User Portion: 0.875 ETH Commission: 0.875 * 5% = 0.04375 ETH Node Share: 0.04375 + 0.125 = 0.16875 ETH Voter Share: 0.875 * 9% = 0.07875 ETH pDAO Share: 0.875 * 1% = 0.00875 ETH rETH Share: 1 - 0.16875 - 0.07875 - 0.00875 = 0.7525 ETH Note: calculations on-chain are of 3 fixed point precision */ const rewardSplit = await megapool.calculatePendingRewards(); assertBN.equal(rewardSplit[0], '0.16875'.ether); // Node assertBN.equal(rewardSplit[1], '0.07875'.ether); // Voter assertBN.equal(rewardSplit[2], '0.00875'.ether); // pDAO assertBN.equal(rewardSplit[3], '0.74375'.ether); // User // Perform distribution await distributeMegapool(megapool); }); it(printTitle('node', 'can not distribute after notify exit'), async () => { // Notify exit in 5 epochs const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 5); // Can't distribute await mockRewards(megapool, '1'.ether); await shouldRevert(distributeMegapool(megapool), 'Was able to distribute', 'Pending validator exit'); }); it(printTitle('node', 'can not notify exit when withdrawable_epoch = FAR_FUTURE_EPOCH'), async () => { await shouldRevert(notifyExitValidator(megapool, 0, farFutureEpoch), 'Notified non-exiting validator', 'Validator not exiting'); }); it(printTitle('node', 'can not notify exit twice'), async () => { const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 5); await shouldRevert(notifyExitValidator(megapool, 0, currentEpoch + 5), 'Notified exit twice', 'Already notified'); }); it(printTitle('node', 'can not notify final balance twice'), async () => { const validatorId = 0n; const withdrawalEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, validatorId, withdrawalEpoch); await notifyFinalBalanceValidator(megapool, validatorId, '32'.ether, owner, withdrawalEpoch * 32); await shouldRevert(notifyFinalBalanceValidator(megapool, validatorId, '32'.ether, owner, withdrawalEpoch * 32), 'Notified final balance twice', 'Already exited'); }); it(printTitle('node', 'can not notify final balance with mismatching validator'), async () => { const withdrawalEpoch = await getCurrentEpoch(); // Collect values needed for notify final balance const validatorId = 0n; const infoBefore = await getValidatorInfo(megapool, validatorId); const executionWithdrawalCredentials = Buffer.from(megapool.target.substr(2), 'hex'); const amountInGwei = '32'.ether / '1'.gwei; const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const withdrawalSlot = withdrawalEpoch * 32; await helpers.mine(); const latestBlock = await ethers.provider.getBlock('latest'); const currentTime = latestBlock.timestamp; // Notify exit await notifyExitValidator(megapool, validatorId, withdrawalEpoch); // Construct proofs const withdrawalProof = { withdrawalSlot: withdrawalSlot, withdrawalNum: 0n, withdrawal: { index: 0n, validatorIndex: 1n, // Not matching validatorProof withdrawalCredentials: executionWithdrawalCredentials, amountInGwei: amountInGwei, }, witnesses: [], }; const validatorProof = { validatorIndex: 2n, // Not matching withdrawalProof validator: { pubkey: infoBefore.pubkey, withdrawalCredentials: withdrawalCredentials, withdrawableEpoch: withdrawalEpoch, // Only above three need to be valid values effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, }, witnesses: [], }; const slotProof = { slot: await getSlotForBlock(), witnesses: [], }; // Mock exiting validator by sending final balance to megapool await owner.sendTransaction({ to: megapool.target, value: '32'.ether, }); const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await shouldRevert( rocketMegapoolManager.connect(node).notifyFinalBalance(megapool.target, validatorId, currentTime, withdrawalProof, validatorProof, slotProof), 'Was able to notify final balance with mismatching validators', 'Withdrawal validator not matching', ); }); it(printTitle('node', 'can not notify final balance from before withdrawal epoch'), async () => { const withdrawalEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, withdrawalEpoch); await shouldRevert( notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, withdrawalEpoch * slotsPerEpoch - 1, withdrawalEpoch), 'Was able to notify final balance prior to withdrawal', 'Not full withdrawal', ); }); it(printTitle('node', 'can not notify exit on exited validator'), async () => { const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, await getCurrentEpoch()); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, currentEpoch * 32); await shouldRevert(notifyExitValidator(megapool, 0, currentEpoch + 5), 'Notified exit twice', 'Already exited'); }); it(printTitle('node', 'can pay off a portion of debt with exiting validator'), async () => { // Apply a penalty larger than bond await votePenalty(megapool, 0n, '8'.ether, trustedNode1); await votePenalty(megapool, 0n, '8'.ether, trustedNode2); // Notify exit of 32 ETH in 113+1 epochs to avoid late penalty const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 114); await helpers.time.increase(12 * 32 * 114); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, await getCurrentEpoch() * 32); // 4 ETH bond should pay down debt, and remaining debt should be 4 ETH assertBN.equal(await megapool.getDebt(), '4'.ether); }); it(printTitle('node', 'can pay off full debt with exiting validator'), async () => { // Apply a penalty larger less than bond await votePenalty(megapool, 0n, '3'.ether, trustedNode1); await votePenalty(megapool, 0n, '3'.ether, trustedNode2); // Notify exit of 32 ETH in 113+1 epochs to avoid late penalty const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 114); await helpers.time.increase(12 * 32 * 114); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, await getCurrentEpoch() * 32); // 4 ETH bond should pay down debt entirely assertBN.equal(await megapool.getDebt(), '0'.ether); }); }); snapshotDescribe('With multiple staking validators', () => { before(async () => { await deployMegapool({ from: node }); for (let i = 0; i < 5; i++) { await nodeDeposit(node); await stakeMegapoolValidator(megapool, i); } }); it(printTitle('node', 'can distribute capital after full exit'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, currentEpoch * 32); // Can distribute await mockRewards(megapool, '1'.ether); await distributeMegapool(megapool); }); it(printTitle('random', 'can not permissionlessly notify final balance and distribute a validator before user distribute delay'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); const megapoolWithRandom = megapool.connect(random); await notifyExitValidator(megapoolWithRandom, 0, currentEpoch); await shouldRevert(notifyFinalBalanceValidator(megapoolWithRandom, 0, '32'.ether, owner, currentEpoch * 32), 'Was able to distribute', 'Not enough time has passed'); }); it(printTitle('random', 'can permissionlessly notify final balance and distribute a validator after user distribute delay'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); const megapoolWithRandom = megapool.connect(random); await notifyExitValidator(megapoolWithRandom, 0, currentEpoch); // Wait the window await helpers.time.increase(userDistributeTime * slotsPerEpoch * secondsPerSlot + 1); // Should work now await notifyFinalBalanceValidator(megapoolWithRandom, 0, '32'.ether, owner, currentEpoch * 32); // Can distribute await mockRewards(megapoolWithRandom, '1'.ether); await distributeMegapool(megapoolWithRandom); }); it(printTitle('random', 'can not permissionlessly notify final balance and distribute a validator with shortfall immediately'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); const megapoolWithRandom = megapool.connect(random); await notifyExitValidator(megapoolWithRandom, 0, currentEpoch); await shouldRevert(notifyFinalBalanceValidator(megapoolWithRandom, 0, '27'.ether, owner, currentEpoch * 32), 'Was able to distribute', 'Not enough time has passed'); }); it(printTitle('random', 'can permissionlessly notify final balance and distribute a validator with shortfall before user distribute with shortfall delay'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); const megapoolWithRandom = megapool.connect(random); await notifyExitValidator(megapoolWithRandom, 0, currentEpoch); // Wait the regular window await helpers.time.increase(userDistributeTime * slotsPerEpoch * secondsPerSlot + 1); // Cannot permissionlessly distribute with shortfall after the standard delay await shouldRevert(notifyFinalBalanceValidator(megapoolWithRandom, 0, '27'.ether, owner, currentEpoch * 32), 'Was able to distribute', 'Not enough time has passed'); }); it(printTitle('random', 'can permissionlessly notify final balance and distribute a validator with shortfall after user distribute with shortfall delay'), async () => { // Notify exit and final balance const currentEpoch = await getCurrentEpoch(); const megapoolWithRandom = megapool.connect(random); await notifyExitValidator(megapoolWithRandom, 0, currentEpoch); // Wait the regular window await helpers.time.increase(userDistributeTime * slotsPerEpoch * secondsPerSlot + 1); // Wait the remaining shortfall delay await helpers.time.increase((userDistributeShortfallTime - userDistributeTime) * slotsPerEpoch * secondsPerSlot + 1); // Should work now await notifyFinalBalanceValidator(megapoolWithRandom, 0, '27'.ether, owner, currentEpoch * 32); // Can distribute await mockRewards(megapoolWithRandom, '1'.ether); await distributeMegapool(megapoolWithRandom); }); it(printTitle('node', 'can bond reduce on exit'), async () => { // Adjust `reduced_bond` to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Notify exit in 5 epochs const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 114); // Increase time to beyond withdrawable_epoch await helpers.time.increase(12 * 32 * 114); const nodeBalanceBefore = await ethers.provider.getBalance(nodeWithdrawalAddress); await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, await getCurrentEpoch() * 32); const nodeBalanceAfter = await ethers.provider.getBalance(nodeWithdrawalAddress); /* NO started with 5 validators old bond requirement = 8 + (4 * 3) = 20 new bond requirement = 8 + (2 * 2) = 12 therefore, NOs bond after exit should be 12, with an 8 ETH refund from the 32 ETH final balance */ const nodeBond = await megapool.getNodeBond(); assertBN.equal(nodeBond, '12'.ether); assertBN.equal(nodeBalanceAfter - nodeBalanceBefore, '8'.ether); }); it(printTitle('node', 'can bond reduce on exit with balance < 32 ETH'), async () => { // Adjust `reduced_bond` to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Notify exit enough into the future to avoid fine const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 114); const nodeBalanceBefore = await ethers.provider.getBalance(nodeWithdrawalAddress); // Increase time to beyond withdrawable_epoch await helpers.time.increase(12 * 32 * 114); await notifyFinalBalanceValidator(megapool, 0, '32'.ether - '7'.ether, owner, await getCurrentEpoch() * 32); const nodeBalanceAfter = await ethers.provider.getBalance(nodeWithdrawalAddress); /* NO should receive 8 ETH bond on exit, but lost 7 ETH capital so bond should reduce by 8 ETH but NO should only receive 1 ETH refund */ const nodeBond = await megapool.getNodeBond(); assertBN.equal(nodeBond, '12'.ether); assertBN.equal(nodeBalanceAfter - nodeBalanceBefore, '1'.ether); }); it(printTitle('node', 'accrues debt when exit balance is too low and bond has been reduced'), async () => { // Adjust `reduced_bond` to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Notify exit in 113+1 epochs to avoid late penalty const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 114); await helpers.time.increase(12 * 32 * 114); await notifyFinalBalanceValidator(megapool, 0, '32'.ether - '9'.ether, owner, await getCurrentEpoch() * 32); /* NO should receive 8 ETH bond on exit, but lost 9 ETH capital so bond should reduce by 8 ETH but NO should accrue a 1 ETH debt */ const nodeBond = await megapool.getNodeBond(); const nodeRefund = await megapool.getRefundValue(); const nodeDebt = await megapool.getDebt(); assertBN.equal(nodeBond, '12'.ether); assertBN.equal(nodeRefund, '0'.ether); assertBN.equal(nodeDebt, '1'.ether); }); it(printTitle('node', 'accrues penalty via debt with late notify exit submission'), async () => { // Set fine to 0.01 ETH const fineAmount = '0.01'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'late.notify.fine', fineAmount, { from: owner }); // Adjust `reduced_bond` to 2 ETH await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner }); // Notify exit in 111 epochs (1 epoch too late) const currentEpoch = await getCurrentEpoch(); await notifyExitValidator(megapool, 0, currentEpoch + 111); /* NO should receive a 0.01 ETH penalty for submitting late */ const nodeDebt = await megapool.getDebt(); assertBN.equal(nodeDebt, fineAmount); // Increase time to beyond withdrawable_epoch const withdrawableEpoch = currentEpoch + 111; await helpers.time.increase(111 * slotsPerEpoch * secondsPerSlot); // Increase time to beyond user distribute window await helpers.time.increase((userDistributeTime + 1) * slotsPerEpoch * slotsPerEpoch + 1); // Submit the final balance from a random account to prevent immediate claim const randomMegapoolRunner = megapool.connect(random); await notifyFinalBalanceValidator(randomMegapoolRunner, 0, '32'.ether, owner, withdrawableEpoch * slotsPerEpoch); // Debt should be paid on exit const nodeDebtAfter = await megapool.getDebt(); assertBN.equal(nodeDebtAfter, 0n); }); snapshotDescribe('With debt', () => { async function getData() { const rocketTokenRETH = await RocketTokenRETH.deployed(); const rocketVault = await RocketVault.deployed(); const [rethBalance, depositPoolBalance, debt, nodeBalance] = await Promise.all([ ethers.provider.getBalance(rocketTokenRETH.target), rocketVault.balanceOf('rocketDepositPool'), megapool.getDebt(), ethers.provider.getBalance(nodeWithdrawalAddress), ]); return { rethBalance, depositPoolBalance, debt, nodeBalance }; } before(async () => { /* Exit the validator with 5 ETH capital loss will result in a debt of 1 ETH */ await exitValidator(megapool, 0, '32'.ether - '5'.ether); const nodeDebt = await megapool.getDebt(); assertBN.equal(nodeDebt, '1'.ether); }); it(printTitle('node', 'can manually pay down debt'), async () => { await repayDebt(megapool, '1'.ether); }); it(printTitle('node', 'can use rewards to partially pay down debt'), async () => { await mockRewards(megapool, '1'.ether); await distributeMegapool(megapool); }); it(printTitle('node', 'can use rewards to fully pay down debt'), async () => { await mockRewards(megapool, '20'.ether); await distributeMegapool(megapool); }); it(printTitle('node', 'will use exit balance to pay down debt'), async () => { const data1 = await getData(); await exitValidator(megapool, 1, '32'.ether); const data2 = await getData(); assertBN.equal(data2.nodeBalance - data1.nodeBalance, '3'.ether); // 3 ETH returned to node assertBN.equal(data2.debt, 0n); // Debt cleared assertBN.equal((data2.rethBalance + data2.depositPoolBalance) - (data1.rethBalance + data1.depositPoolBalance), '28'.ether + '1'.ether); // User capital + 1 ETH debt returned to rETH }); it(printTitle('node', 'will increase debt further on slashed exit'), async () => { const data1 = await getData(); await exitValidator(megapool, 1, '27'.ether); const data2 = await getData(); assertBN.equal(data2.nodeBalance - data1.nodeBalance, '0'.ether); // No change assertBN.equal(data2.debt - data1.debt, '1'.ether); // 1 ETH more debt added assertBN.equal((data2.rethBalance + data2.depositPoolBalance) - (data1.rethBalance + data1.depositPoolBalance), '27'.ether); // Entire balance sent to rETH }); }); }); }); snapshotDescribe('With upgraded delegate', () => { let upgradeHelper; let oldDelegate, newDelegate; before(async () => { await deployMegapool({ from: node }); const rocketStorage = await RocketStorage.deployed(); upgradeHelper = await MegapoolUpgradeHelper.deployed(); oldDelegate = await RocketMegapoolDelegate.deployed(); newDelegate = await RocketMegapoolDelegate.clone(rocketStorage.target); }); it(printTitle('random', 'can not upgrade non-expired delegate'), async () => { // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); // Try to upgrade await shouldRevert(megapool.connect(random).delegateUpgrade(), 'Was able to upgrade delegate', 'Only the node operator can access this method'); }); it(printTitle('random', 'can upgrade expired delegate'), async () => { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); // Fast-forward until delegate expires const oldDelegateExpiry = await rocketMegapoolFactory.getDelegateExpiry(oldDelegate.target); await helpers.mineUpTo(oldDelegateExpiry + 1n); // Try to upgrade await megapool.connect(random).delegateUpgrade(); }); it(printTitle('node', 'can upgrade non-expired delegate'), async () => { // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); // Try to upgrade await megapool.delegateUpgrade(); }); it(printTitle('node', 'can upgrade expired delegate'), async () => { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); // Fast-forward until delegate expires const oldDelegateExpiry = await rocketMegapoolFactory.getDelegateExpiry(oldDelegate.target); await helpers.mineUpTo(oldDelegateExpiry + 1n); // Try to upgrade await megapool.delegateUpgrade(); }); it(printTitle('node', 'expired delegate automatically upgrades'), async () => { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); const oldDelegateExpiry = await rocketMegapoolFactory.getDelegateExpiry(oldDelegate.target); // Fast-forward until just before delegate expires await helpers.time.increaseTo(oldDelegateExpiry - 10n); await megapool.connect(node).claim(); // Check delegate is old assert.equal(await megapool.connect(node).getDelegate(), oldDelegate.target); // Fast-forward until after delegate expires await helpers.time.increaseTo(oldDelegateExpiry + 10n); // Check effective delegate is the new one assert.equal(await megapool.connect(node).getEffectiveDelegate(), newDelegate.target); // Execute a method to force the upgrade await megapool.connect(node).claim(); // Check stored delegate is new assert.equal(await megapool.connect(node).getDelegate(), newDelegate.target); }); it(printTitle('node', 'can use latest delegate'), async () => { const rocketMegapoolFactory = await RocketMegapoolFactory.deployed(); // Enable useLatest await megapool.setUseLatestDelegate(true); // Execute delegate upgrade via helper contract await upgradeHelper.upgradeDelegate(newDelegate.target); // Fast-forward until delegate expires const oldDelegateExpiry = await rocketMegapoolFactory.getDelegateExpiry(oldDelegate.target); await helpers.mineUpTo(oldDelegateExpiry + 1n); // Should be able to call a function without reverting await megapool.connect(node).getValidatorCount(); // Effective delegate should be latest assert.equal(await megapool.getEffectiveDelegate(), newDelegate.target); // Disable use latest and check delegate remains the latest await megapool.setUseLatestDelegate(false); assert.equal(await megapool.getEffectiveDelegate(), newDelegate.target); assert.equal(await megapool.getDelegate(), newDelegate.target); }); it(printTitle('node', 'can not set use latest with current value'), async () => { await megapool.setUseLatestDelegate(true); await shouldRevert( megapool.setUseLatestDelegate(true), 'Was able to set to current value', 'Already set', ); }); }); }); } ================================================ FILE: test/megapool/scenario-apply-penalty.js ================================================ import { RocketDAONodeTrusted, RocketMegapoolPenalties } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Vote to apply a penalty to a megapool export async function votePenalty(megapool, slot, amount, trustedNode) { const rocketMegapoolPenalties = await RocketMegapoolPenalties.deployed(); const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); async function getBalances() { let [voteCount, nodeDebt, currentMaxPenalty, currentPenaltyRunningTotal] = await Promise.all([ rocketMegapoolPenalties.getVoteCount(megapool.target, slot, amount), megapool.getDebt(), rocketMegapoolPenalties.getCurrentMaxPenalty(), rocketMegapoolPenalties.getCurrentPenaltyRunningTotal(), ]); return { voteCount, nodeDebt, currentMaxPenalty, currentPenaltyRunningTotal }; } const balancesBefore = await getBalances(); await rocketMegapoolPenalties.connect(trustedNode).penalise(megapool.target, slot, amount); const balancesAfter = await getBalances(); const balanceDeltas = { voteCount: balancesAfter.voteCount - balancesBefore.voteCount, nodeDebt: balancesAfter.nodeDebt - balancesBefore.nodeDebt, currentMaxPenalty: balancesAfter.currentMaxPenalty - balancesBefore.currentMaxPenalty, currentPenaltyRunningTotal: balancesAfter.currentPenaltyRunningTotal - balancesBefore.currentPenaltyRunningTotal, }; let expectedPenalty = 0n; if (balancesAfter.voteCount > trustedNodeCount / 2n) { expectedPenalty = amount; if (expectedPenalty > balancesBefore.currentMaxPenalty) { expectedPenalty = balancesBefore.currentMaxPenalty; } } assertBN.equal(balanceDeltas.nodeDebt, expectedPenalty); // Debt should increase by expected penalty assertBN.equal(balanceDeltas.voteCount, 1n); // Vote count should increment assertBN.equal(balanceDeltas.currentMaxPenalty, -expectedPenalty); // Max penalty should reduce by penalty assertBN.equal(balanceDeltas.currentPenaltyRunningTotal, expectedPenalty); // Running total should increase by penalty } ================================================ FILE: test/megapool/scenario-challenge.js ================================================ import { RocketMegapoolManager } from '../_utils/artifacts'; import { getValidatorInfo } from '../_helpers/megapool'; import assert from 'assert'; import { assertBN } from '../_helpers/bn'; export async function challengeValidator(megapool, validatorIds, challenger) { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); async function getData() { let [ lockedCount, infos ] = await Promise.all([ megapool.getLockedValidatorCount(), Promise.all(validatorIds.map(id => getValidatorInfo(megapool, id))) ]) return { lockedCount, infos }; } const data1 = await getData(); await rocketMegapoolManager.connect(challenger).challengeExit([ { megapool, validatorIds }, ]); const data2 = await getData(); // Check last challenger was updated const lastChallenger = await rocketMegapoolManager.getLastChallenger(); assert.equal(lastChallenger.toLowerCase(), challenger.address.toLowerCase()); // Check number of locked validators let newlyLockedCount = 0 for (let i = 0; i < validatorIds.length; ++i) { newlyLockedCount += !data1.infos[i].locked ? 1 : 0 assert.equal(data2.infos[i].locked, true) } assertBN.equal(data2.lockedCount - data1.lockedCount, BigInt(newlyLockedCount)) } ================================================ FILE: test/megapool/scenario-dissolve.js ================================================ import { getMegapoolForNode } from '../_helpers/megapool'; import { RocketDAOProtocolSettingsMegapool, RocketMegapoolManager, RocketNodeDeposit, RocketNodeStaking, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { getSlotForBlock } from '../_helpers/beaconchain'; import { checkMegapoolInvariants } from '../_helpers/invariants'; const hre = require('hardhat'); const ethers = hre.ethers; const helpers = require('@nomicfoundation/hardhat-network-helpers'); export async function dissolveValidator(node, validatorIndex, from = node, proof = null) { const megapool = await getMegapoolForNode(node); const [ rocketNodeStaking, rocketNodeDeposit, rocketDAOProtocolSettingsMegapool, ] = await Promise.all([ RocketNodeStaking.deployed(), RocketNodeDeposit.deployed(), RocketDAOProtocolSettingsMegapool.deployed(), ]); const nodeAddress = await megapool.getNodeAddress(); const dissolvePenalty = await rocketDAOProtocolSettingsMegapool.getDissolvePenalty(); async function getData() { return await Promise.all([ rocketNodeStaking.getNodeETHBorrowed(nodeAddress), rocketNodeStaking.getNodeETHBonded(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBorrowed(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBonded(nodeAddress), megapool.getNodeBond(), megapool.getNodeQueuedBond(), megapool.getUserCapital(), megapool.getUserQueuedCapital(), megapool.getDebt(), ]).then( ([nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, nodeQueuedBond, userCapital, userQueuedCapital, debt]) => ({ nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, nodeQueuedBond, userCapital, userQueuedCapital, debt, }), ); } // Calculate new bond requirement const activeValidatorCount = await megapool.getActiveValidatorCount(); let bondRequirement = 0n; if (activeValidatorCount > 1n) { bondRequirement = await rocketNodeDeposit.getBondRequirement(activeValidatorCount - 1n); } const nodeBond = await megapool.getNodeBond(); const nodeQueuedBond = await megapool.getNodeQueuedBond(); const effectiveNodeBond = nodeBond + nodeQueuedBond; // Calculate expected change in bond and capital let expectedNodeBondChange let expectedDebtChange = dissolvePenalty if (effectiveNodeBond <= bondRequirement) { // When underbonded, the 32 ETH goes directly to user capital expectedNodeBondChange = 0n; // But 1 ETH is lost, so the NO accrues a debt expectedDebtChange += '1'.ether } else { expectedNodeBondChange = bondRequirement - effectiveNodeBond; if (expectedNodeBondChange < -'32'.ether) { expectedNodeBondChange = -'32'.ether; } if (expectedNodeBondChange < -nodeBond) { expectedNodeBondChange = -nodeBond; } } const expectedUserCapitalChange = -'32'.ether - expectedNodeBondChange; const data1 = await getData(); if (proof === null) { await megapool.connect(from).dissolveValidator(validatorIndex); } else { // Use current time as slot timestamp await helpers.mine(); const latestBlock = await ethers.provider.getBlock('latest'); const currentTime = latestBlock.timestamp; const slotProof = { slot: await getSlotForBlock(), witnesses: [], }; const rocketMegapoolManager = await RocketMegapoolManager.deployed(); await rocketMegapoolManager.connect(from).dissolve(megapool.target, validatorIndex, currentTime, proof, slotProof); } const data2 = await getData(); const deltas = { nodeEthBonded: data2.nodeEthBonded - data1.nodeEthBonded, nodeEthBorrowed: data2.nodeEthBorrowed - data1.nodeEthBorrowed, nodeMegapoolEthBonded: data2.nodeMegapoolEthBonded - data1.nodeMegapoolEthBonded, nodeMegapoolEthBorrowed: data2.nodeMegapoolEthBorrowed - data1.nodeMegapoolEthBorrowed, nodeBond: data2.nodeBond - data1.nodeBond, userCapital: data2.userCapital - data1.userCapital, debt: data2.debt - data1.debt, }; assertBN.equal(deltas.nodeEthBonded, expectedNodeBondChange); assertBN.equal(deltas.nodeEthBorrowed, expectedUserCapitalChange); assertBN.equal(deltas.nodeMegapoolEthBonded, expectedNodeBondChange); assertBN.equal(deltas.nodeMegapoolEthBorrowed, expectedUserCapitalChange); assertBN.equal(deltas.nodeBond, expectedNodeBondChange); assertBN.equal(deltas.userCapital, expectedUserCapitalChange); assertBN.equal(data2.userCapital + data2.userQueuedCapital, data2.nodeMegapoolEthBorrowed); assertBN.equal(data2.nodeBond + data2.nodeQueuedBond, data2.nodeMegapoolEthBonded); assertBN.equal(deltas.nodeBond + deltas.userCapital, -'32'.ether); assertBN.equal(deltas.debt, expectedDebtChange); await checkMegapoolInvariants() } ================================================ FILE: test/megapool/scenario-distribute.js ================================================ import { RocketNetworkRevenues, RocketStorage, RocketTokenRETH, RocketVault, RocketVoterRewards, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { checkMegapoolInvariants } from '../_helpers/invariants'; const hre = require('hardhat'); const ethers = hre.ethers; // Distribute a megapool export async function distributeMegapool(megapool) { const rocketStorage = await RocketStorage.deployed(); const rocketTokenRETH = await RocketTokenRETH.deployed(); const rocketVault = await RocketVault.deployed(); const nodeAddress = await megapool.getNodeAddress(); const withdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(nodeAddress); async function getBalances() { let [pendingRewards, megapoolBalance, nodeBalance, voterBalance, pdaoBalance, rethBalance, nodeDebt, refundValue] = await Promise.all([ megapool.getPendingRewards(), ethers.provider.getBalance(megapool.target), ethers.provider.getBalance(withdrawalAddress), rocketVault.balanceOf("rocketRewardsPool"), rocketVault.balanceOf("rocketClaimDAO"), ethers.provider.getBalance(rocketTokenRETH.target), megapool.getDebt(), megapool.getRefundValue(), ]); return { pendingRewards, megapoolBalance, nodeBalance, voterBalance, pdaoBalance, rethBalance, nodeDebt, refundValue }; } const [expectedNodeRewards, expectedVoterRewards, expectedProtocolDAORewards, expectedRethRewards] = await megapool.calculatePendingRewards(); const balancesBefore = await getBalances(); await megapool.distribute(); const balancesAfter = await getBalances(); const balanceDeltas = { pendingRewards: balancesAfter.pendingRewards - balancesBefore.pendingRewards, megapoolBalance: balancesAfter.megapoolBalance - balancesBefore.megapoolBalance, nodeBalance: balancesAfter.nodeBalance - balancesBefore.nodeBalance, voterBalance: balancesAfter.voterBalance - balancesBefore.voterBalance, pdaoBalance: balancesAfter.pdaoBalance - balancesBefore.pdaoBalance, rethBalance: balancesAfter.rethBalance - balancesBefore.rethBalance, nodeDebt: balancesAfter.nodeDebt - balancesBefore.nodeDebt, refundValue: balancesAfter.refundValue - balancesBefore.refundValue, } let expectedDebtDelta = 0n; if (balancesBefore.nodeDebt > 0n) { if (balancesBefore.nodeDebt > expectedNodeRewards) { expectedDebtDelta = -expectedNodeRewards; } else { expectedDebtDelta = -balancesBefore.nodeDebt; } } const nodeCalling = (megapool.runner.address.toLowerCase() === nodeAddress.toLowerCase()) || (megapool.runner.address.toLowerCase() === withdrawalAddress.toLowerCase()); if (nodeCalling) { assertBN.equal(balanceDeltas.nodeBalance - balanceDeltas.nodeDebt, expectedNodeRewards); } else { assertBN.equal(balanceDeltas.refundValue - balanceDeltas.nodeDebt, expectedNodeRewards); } assertBN.equal(balanceDeltas.nodeDebt, expectedDebtDelta); assertBN.equal(balanceDeltas.rethBalance, -expectedDebtDelta + expectedRethRewards); assertBN.equal(balanceDeltas.voterBalance, expectedVoterRewards); assertBN.equal(balanceDeltas.pdaoBalance, expectedProtocolDAORewards); assertBN.equal(balanceDeltas.rethBalance + balanceDeltas.nodeDebt, expectedRethRewards); assertBN.equal(balancesAfter.pendingRewards, 0n); await checkMegapoolInvariants() } ================================================ FILE: test/megapool/scenario-exit-queue.js ================================================ import { getMegapoolForNode, getValidatorInfo } from '../_helpers/megapool'; import assert from 'assert'; import { RocketDepositPool, RocketNodeDeposit, RocketNodeStaking } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { checkMegapoolInvariants } from '../_helpers/invariants'; const launchValue = '32'.ether; const milliToWei = 1000000000000000n; export async function exitQueue(nodeAddress, validatorIndex) { const megapool = await getMegapoolForNode(nodeAddress) const rocketNodeStaking = await RocketNodeStaking.deployed(); const rocketDepositPool = await RocketDepositPool.deployed(); async function getData() { return await Promise.all([ rocketNodeStaking.getNodeETHBorrowed(nodeAddress), rocketNodeStaking.getNodeETHBonded(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBorrowed(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBonded(nodeAddress), megapool.getNodeBond(), megapool.getUserCapital(), megapool.getNodeQueuedBond(), megapool.getUserQueuedCapital(), megapool.getActiveValidatorCount(), rocketDepositPool.getNodeCreditBalance(nodeAddress) ]).then( ([nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, validatorCount, nodeCredit]) => ({ nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, validatorCount, nodeCredit }), ); } const activeValidatorCountBefore = await megapool.getActiveValidatorCount(); const validatorInfo = await getValidatorInfo(megapool, validatorIndex) // Dequeue the validator const data1 = await getData(); await megapool.dequeue(validatorIndex); const data2 = await getData(); const activeValidatorCount = await megapool.getActiveValidatorCount(); // Calculate expected change in bond and capital const lastRequestedBond = BigInt(validatorInfo.lastRequestedBond) * milliToWei; const lastRequestedValue = BigInt(validatorInfo.lastRequestedValue) * milliToWei; const expectedNodeBondChange = -lastRequestedBond; const expectedUserCapitalChange = -(lastRequestedValue - lastRequestedBond); const expectedCredit = lastRequestedBond; // Check the validator status const validatorInfoAfter = await megapool.getValidatorInfo(validatorIndex); assert.equal(validatorInfoAfter.staked, false); assert.equal(validatorInfoAfter.inQueue, false); const deltas = { nodeEthBonded: data2.nodeEthBonded - data1.nodeEthBonded, nodeEthBorrowed: data2.nodeEthBorrowed - data1.nodeEthBorrowed, nodeMegapoolEthBonded: data2.nodeMegapoolEthBonded - data1.nodeMegapoolEthBonded, nodeMegapoolEthBorrowed:data2.nodeMegapoolEthBorrowed - data1.nodeMegapoolEthBorrowed, nodeBond:data2.nodeBond - data1.nodeBond, userCapital: data2.userCapital - data1.userCapital, nodeQueuedBond: data2.nodeQueuedBond - data1.nodeQueuedBond, userQueuedCapital: data2.userQueuedCapital - data1.userQueuedCapital, nodeCredit: data2.nodeCredit - data1.nodeCredit, } assertBN.equal(deltas.nodeEthBonded, expectedNodeBondChange); assertBN.equal(deltas.nodeEthBorrowed, expectedUserCapitalChange); assertBN.equal(deltas.nodeMegapoolEthBonded, expectedNodeBondChange); assertBN.equal(deltas.nodeMegapoolEthBorrowed, expectedUserCapitalChange); assertBN.equal(deltas.nodeBond, 0n); assertBN.equal(deltas.nodeQueuedBond, expectedNodeBondChange); assertBN.equal(deltas.userCapital, 0n); assertBN.equal(deltas.userQueuedCapital, expectedUserCapitalChange); assertBN.equal(data2.userCapital + data2.userQueuedCapital, data2.nodeMegapoolEthBorrowed); assertBN.equal(data2.nodeBond + data2.nodeQueuedBond, data2.nodeMegapoolEthBonded); assertBN.equal(deltas.nodeBond + deltas.userCapital + deltas.nodeQueuedBond + deltas.userQueuedCapital, -launchValue); assertBN.equal(deltas.nodeCredit, expectedCredit); assertBN.equal(activeValidatorCount, activeValidatorCountBefore - 1n); await checkMegapoolInvariants() } ================================================ FILE: test/megapool/scenario-exit.js ================================================ import { RocketMegapoolManager, RocketNodeDeposit, RocketStorage, RocketTokenRETH, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { getValidatorInfo } from '../_helpers/megapool'; import assert from 'assert'; import { getSlotForBlock, slotsPerEpoch } from '../_helpers/beaconchain'; import { checkMegapoolInvariants } from '../_helpers/invariants'; const hre = require('hardhat'); const ethers = hre.ethers; const helpers = require('@nomicfoundation/hardhat-network-helpers'); // Notify megapool of exiting validator export async function notifyExitValidator(megapool, validatorId, withdrawalEpoch) { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); async function getData() { let [activeValidatorCount, exitingValidatorCount] = await Promise.all([ megapool.getActiveValidatorCount(), megapool.getExitingValidatorCount(), ]); return { activeValidatorCount, exitingValidatorCount }; } const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const infoBefore = await getValidatorInfo(megapool, validatorId); // Use current time as slot timestamp await helpers.mine(); const latestBlock = await ethers.provider.getBlock('latest'); const currentTime = latestBlock.timestamp; // Construct a fake proof const proof = { validatorIndex: 1n, validator: { pubkey: infoBefore.pubkey, withdrawalCredentials: withdrawalCredentials, effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, withdrawableEpoch: withdrawalEpoch, }, witnesses: [], }; const slotProof = { slot: await getSlotForBlock(), witnesses: [], }; const dataBefore = await getData(); await rocketMegapoolManager.notifyExit(megapool.target, validatorId, currentTime, proof, slotProof); const dataAfter = await getData(); const info = await getValidatorInfo(megapool, validatorId); const deltas = { activeValidatorCount: dataAfter.activeValidatorCount - dataBefore.activeValidatorCount, exitingValidatorCount: dataAfter.exitingValidatorCount - dataBefore.exitingValidatorCount, }; assert.equal(info.exiting, true); assert.equal(info.exited, false); assertBN.equal(deltas.activeValidatorCount, 0n); assertBN.equal(deltas.exitingValidatorCount, 1n); } // Notify validator of final balance export async function notifyFinalBalanceValidator(megapool, validatorId, finalBalance, funder, withdrawalSlot, withdrawableEpoch = null) { const rocketStorage = await RocketStorage.deployed(); const rocketTokenRETH = await RocketTokenRETH.deployed(); const rocketMegapoolManager = await RocketMegapoolManager.deployed(); const rocketVault = await RocketVault.deployed(); const rocketNodeDeposit = await RocketNodeDeposit.deployed(); const nodeAddress = await megapool.getNodeAddress(); const withdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(nodeAddress); async function getData() { let [pendingRewards, megapoolBalance, nodeBalance, rethBalance, depositPoolBalance, nodeRefund, activeValidatorCount, exitingValidatorCount, nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, nodeDebt] = await Promise.all([ megapool.getPendingRewards(), ethers.provider.getBalance(megapool.target), ethers.provider.getBalance(withdrawalAddress), ethers.provider.getBalance(rocketTokenRETH.target), rocketVault.balanceOf('rocketDepositPool'), megapool.getRefundValue(), megapool.getActiveValidatorCount(), megapool.getExitingValidatorCount(), megapool.getNodeBond(), megapool.getUserCapital(), megapool.getNodeQueuedBond(), megapool.getUserQueuedCapital(), megapool.getDebt(), ]); return { pendingRewards, megapoolBalance, nodeBalance, rethBalance, depositPoolBalance, nodeRefund, activeValidatorCount, exitingValidatorCount, nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, nodeDebt, }; } const data1 = await getData(); // Mock exiting validator by sending final balance to megapool await funder.sendTransaction({ to: megapool.target, value: finalBalance, }); const executionWithdrawalCredentials = Buffer.from(megapool.target.substr(2), 'hex'); const amountInGwei = finalBalance / '1'.gwei; const infoBefore = await getValidatorInfo(megapool, validatorId); const withdrawalCredentials = await megapool.getWithdrawalCredentials(); // Use current time as slot timestamp await helpers.mine(); const latestBlock = await ethers.provider.getBlock('latest'); const currentTime = latestBlock.timestamp; const withdrawalProof = { withdrawalSlot: withdrawalSlot, withdrawalNum: 0n, withdrawal: { index: 0n, validatorIndex: 2n, withdrawalCredentials: executionWithdrawalCredentials, amountInGwei: amountInGwei, }, witnesses: [], }; const validatorProof = { validatorIndex: 2n, validator: { pubkey: infoBefore.pubkey, withdrawalCredentials: withdrawalCredentials, withdrawableEpoch: withdrawableEpoch ? withdrawableEpoch : withdrawalSlot / slotsPerEpoch, // Only above three need to be valid values effectiveBalance: 0n, slashed: false, activationEligibilityEpoch: 0n, activationEpoch: 0n, exitEpoch: 0n, }, witnesses: [], }; const slotProof = { slot: await getSlotForBlock(), witnesses: [], }; await rocketMegapoolManager.connect(megapool.runner).notifyFinalBalance(megapool.target, validatorId, currentTime, withdrawalProof, validatorProof, slotProof); const data2 = await getData(); // Calculate new bond requirement let bondRequirement = 0n; if (data2.activeValidatorCount > 0n) { bondRequirement = await rocketNodeDeposit.getBondRequirement(data2.activeValidatorCount); } const effectiveNodeBond = data1.nodeBond + data1.nodeQueuedBond; // Calculate expected change in bond and capital let expectedNodeBondChange let expectedDebtChange = '0'.ether if (effectiveNodeBond <= bondRequirement) { // When underbonded, the 32 ETH goes directly to user capital expectedNodeBondChange = 0n; } else { expectedNodeBondChange = bondRequirement - effectiveNodeBond; if (expectedNodeBondChange < -'32'.ether) { expectedNodeBondChange = -'32'.ether; } if (expectedNodeBondChange < -data1.nodeBond) { expectedNodeBondChange = -data1.nodeBond; } } const expectedUserCapitalChange = -'32'.ether - expectedNodeBondChange; const deltas = { pendingRewards: data2.pendingRewards - data1.pendingRewards, megapoolBalance: data2.megapoolBalance - data1.megapoolBalance, nodeBalance: data2.nodeBalance - data1.nodeBalance, rethBalance: data2.rethBalance - data1.rethBalance, depositPoolBalance: data2.depositPoolBalance - data1.depositPoolBalance, nodeRefund: data2.nodeRefund - data1.nodeRefund, activeValidatorCount: data2.activeValidatorCount - data1.activeValidatorCount, nodeBond: data2.nodeBond - data1.nodeBond, userCapital: data2.userCapital - data1.userCapital, nodeQueuedBond: data2.nodeQueuedBond - data1.nodeQueuedBond, userQueuedCapital: data2.userQueuedCapital - data1.userQueuedCapital, }; const nodeCalling = (megapool.runner.address.toLowerCase() === nodeAddress.toLowerCase()) || (megapool.runner.address.toLowerCase() === withdrawalAddress.toLowerCase()); // Check state updates const info = await getValidatorInfo(megapool, validatorId); assert.equal(info.exiting, false); assert.equal(info.exited, true); assertBN.equal(info.exitBalance, finalBalance / '1'.gwei); if (info.dissolved) { assertBN.equal(deltas.nodeBond, 0n); assertBN.equal(deltas.userCapital, 0n); assertBN.equal(deltas.nodeQueuedBond, 0n); assertBN.equal(deltas.userQueuedCapital, 0n); } else { assertBN.equal(deltas.nodeBond, expectedNodeBondChange); assertBN.equal(deltas.userCapital, expectedUserCapitalChange); assertBN.equal(deltas.nodeQueuedBond, 0n); assertBN.equal(deltas.userQueuedCapital, 0n); } if (!info.dissolved){ // Pending rewards shouldn't change on capital distribution assertBN.equal(deltas.pendingRewards, 0); if (nodeCalling) { assertBN.equal(deltas.depositPoolBalance + deltas.rethBalance + deltas.nodeBalance + deltas.nodeRefund, finalBalance); } else { assertBN.equal(deltas.depositPoolBalance + deltas.rethBalance + deltas.nodeRefund, finalBalance); } assertBN.equal(deltas.activeValidatorCount, -1n); } await checkMegapoolInvariants() } ================================================ FILE: test/megapool/scenario-reduce-bond.js ================================================ // Reduce bond import { RocketNodeStaking } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { checkMegapoolInvariants } from '../_helpers/invariants'; export async function reduceBond(megapool, amount) { const [ rocketNodeStaking, ] = await Promise.all([ RocketNodeStaking.deployed(), ]); const nodeAddress = await megapool.getNodeAddress(); async function getData() { return await Promise.all([ rocketNodeStaking.getNodeETHBorrowed(nodeAddress), rocketNodeStaking.getNodeETHBonded(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBorrowed(nodeAddress), rocketNodeStaking.getNodeMegapoolETHBonded(nodeAddress), megapool.getNodeBond(), megapool.getUserCapital(), ]).then( ([nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, userCapital]) => ({ nodeEthBorrowed, nodeEthBonded, nodeMegapoolEthBorrowed, nodeMegapoolEthBonded, nodeBond, userCapital }), ); } const data1 = await getData(); await megapool.reduceBond(amount); const data2 = await getData(); const nodeEthBondedDelta = data2.nodeEthBonded - data1.nodeEthBonded; const nodeEthBorrowedDelta = data2.nodeEthBorrowed - data1.nodeEthBorrowed; const nodeMegapoolEthBondedDelta = data2.nodeMegapoolEthBonded - data1.nodeMegapoolEthBonded; const nodeMegapoolEthBorrowedDelta = data2.nodeMegapoolEthBorrowed - data1.nodeMegapoolEthBorrowed; const nodeBondDelta = data2.nodeBond - data1.nodeBond; const userCapitalDelta = data2.userCapital - data1.userCapital; assertBN.equal(nodeBondDelta, -amount); assertBN.equal(userCapitalDelta, amount); assertBN.equal(nodeEthBondedDelta, -amount); assertBN.equal(nodeEthBorrowedDelta, amount); assertBN.equal(nodeMegapoolEthBondedDelta, -amount); assertBN.equal(nodeMegapoolEthBorrowedDelta, amount); await checkMegapoolInvariants() } ================================================ FILE: test/megapool/scenario-repay-debt.js ================================================ import { RocketTokenRETH } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; export async function repayDebt(megapool, amount) { const rocketTokenRETH = await RocketTokenRETH.deployed() async function getData() { const [ debt, rethBalance ] = await Promise.all([ megapool.getDebt(), ethers.provider.getBalance(rocketTokenRETH.target), ]) return {debt, rethBalance}; } const data1 = await getData() await megapool.repayDebt({ value: amount }); const data2 = await getData() assertBN.equal(data2.debt, data1.debt - amount); const balanceDelta = data2.rethBalance - data1.rethBalance; assertBN.equal(balanceDelta, data1.debt) } ================================================ FILE: test/megapool/scenario-stake.js ================================================ import { getValidatorInfo } from '../_helpers/megapool'; import assert from 'assert'; import { assertBN } from '../_helpers/bn'; import { RocketMegapoolManager } from '../_utils/artifacts'; import { checkMegapoolInvariants } from '../_helpers/invariants'; const milliToWei = 1000000000000000n; const prestakeBalance = 1000000000n; const hre = require('hardhat'); const ethers = hre.ethers; let validatorIndex = 0; const prestakeAmount = '1'.ether; const farFutureEpoch = '18446744073709551615'.BN; // Stake a megapool validator export async function stakeMegapoolValidator(megapool, index) { const rocketMegapoolManager = await RocketMegapoolManager.deployed(); async function getData() { return await Promise.all([ megapool.getNodeBond(), megapool.getUserCapital(), megapool.getNodeQueuedBond(), megapool.getUserQueuedCapital(), megapool.getActiveValidatorCount(), megapool.getAssignedValue(), ]).then( ([nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, validatorCount, assignedValue]) => ({ nodeBond, userCapital, nodeQueuedBond, userQueuedCapital, validatorCount, assignedValue }), ); } // Gather info const withdrawalCredentials = await megapool.getWithdrawalCredentials(); const validatorInfo = await getValidatorInfo(megapool, index); // Construct a fake proof const proof = { validatorIndex: validatorIndex++, validator: { pubkey: validatorInfo.pubkey, withdrawalCredentials: withdrawalCredentials, withdrawableEpoch: farFutureEpoch, effectiveBalance: prestakeBalance, slashed: false, activationEligibilityEpoch: farFutureEpoch, activationEpoch: farFutureEpoch, exitEpoch: farFutureEpoch, }, witnesses: [], }; const slotProof = { slot: 0n, witnesses: [], }; const infoBefore = await getValidatorInfo(megapool, index); const lastAssignedValue = infoBefore.lastRequestedValue * milliToWei; // Get current time const latestBlock = await ethers.provider.getBlock('latest'); const currentTime = latestBlock.timestamp; // Perform stake operation const data1 = await getData(); await rocketMegapoolManager.stake(megapool.target, index, currentTime, proof, slotProof); const data2 = await getData(); // Check state changes const lastDistributionTime = await megapool.getLastDistributionTime(); const info = await getValidatorInfo(megapool, index); assert.equal(info.staked, true); assert.equal(info.inQueue, false); assert.equal(info.inPrestake, false); assert.equal(info.dissolved, false); const deltas = { nodeBond: data2.nodeBond - data1.nodeBond, userCapital: data2.userCapital - data1.userCapital, nodeQueuedBond: data2.nodeQueuedBond - data1.nodeQueuedBond, userQueuedCapital: data2.userQueuedCapital - data1.userQueuedCapital, validatorCount: data2.validatorCount - data1.validatorCount, assignedValue: data2.assignedValue - data1.assignedValue, }; assertBN.equal(deltas.nodeBond, 0n); assertBN.equal(deltas.userCapital, 0n); assertBN.equal(deltas.userQueuedCapital, 0n); assertBN.equal(deltas.nodeQueuedBond, 0n); assertBN.equal(deltas.assignedValue, -(lastAssignedValue - prestakeAmount)); assertBN.equal(deltas.validatorCount, 0n); assertBN.notEqual(lastDistributionTime, 0n); await checkMegapoolInvariants(); } ================================================ FILE: test/megapool/scenario-withdraw-credit.js ================================================ import { RocketDAOProtocolSettingsDeposit, RocketDepositPool, RocketStorage, RocketTokenRETH, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { checkMegapoolInvariants } from '../_helpers/invariants'; export async function withdrawCredit(node, amount, from = node) { const rocketDepositPool = await RocketDepositPool.deployed(); const rocketStorage = await RocketStorage.deployed(); const rocketTokenRETH = await RocketTokenRETH.deployed(); const rocketDAOProtocolSettingsDeposit = await RocketDAOProtocolSettingsDeposit.deployed(); const depositFeePerc = await rocketDAOProtocolSettingsDeposit.getDepositFee(); async function getData() { const [ rethBalance, creditBalance, userBalance, ] = await Promise.all([ rocketTokenRETH.balanceOf(withdrawalAddress), rocketDepositPool.getNodeCreditBalance(node.address), rocketDepositPool.getUserBalance() ]); return {rethBalance, creditBalance, userBalance}; } const calcBase = '1'.ether; const depositFee = amount * depositFeePerc / calcBase; const amountAfterFee = amount - depositFee const withdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(node.address); const rethValue = await rocketTokenRETH.getRethValue(amountAfterFee); const dataBefore = await getData(); if (from === node) { await rocketDepositPool.connect(from).withdrawCredit(amount); } else { await rocketDepositPool.connect(from).withdrawCreditFor(node.address, amount); } const dataAfter = await getData(); const rethBalanceDelta = dataAfter.rethBalance - dataBefore.rethBalance; const creditBalanceDelta = dataAfter.creditBalance - dataBefore.creditBalance; const userBalanceDelta = dataAfter.userBalance - dataBefore.userBalance; assertBN.equal(rethBalanceDelta, rethValue); assertBN.equal(creditBalanceDelta, -amount); assertBN.equal(userBalanceDelta, 0n); await checkMegapoolInvariants() } ================================================ FILE: test/minipool/minipool-scrub-tests.js ================================================ import { before, describe, it } from 'mocha'; import { RocketDAONodeTrustedSettingsMinipool, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, } from '../_utils/artifacts'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { userDeposit } from '../_helpers/deposit'; import { createMinipool, getMinipoolMinimumRPLStake, stakeMinipool } from '../_helpers/minipool'; import { nodeStakeRPL, registerNode, setNodeTrusted, setNodeWithdrawalAddress } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { close } from './scenario-close'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { voteScrub } from './scenario-scrub'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketMinipool', () => { let owner, node, nodeWithdrawalAddress, trustedNode1, trustedNode2, trustedNode3, random; // Setup let launchTimeout = (60 * 60 * 72); // 72 hours let withdrawalDelay = 20; let scrubPeriod = (60 * 60 * 24); // 24 hours let minipoolSalt = 1; let prelaunchMinipool; before(async () => { await globalSnapShot(); [ owner, node, nodeWithdrawalAddress, trustedNode1, trustedNode2, trustedNode3, random, ] = await ethers.getSigners(); // Register node & set withdrawal address await registerNode({ from: node }); await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, { from: node }); // Register trusted nodes await registerNode({ from: trustedNode1 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); await registerNode({ from: trustedNode2 }); await setNodeTrusted(trustedNode2, 'saas_2', 'node@home.com', owner); await registerNode({ from: trustedNode3 }); await setNodeTrusted(trustedNode3, 'saas_3', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.withdrawal.delay', withdrawalDelay, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, { from: owner }); // Set rETH collateralisation target to a value high enough it won't cause excess ETH to be funneled back into deposit pool and mess with our calcs await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '50'.ether, { from: owner }); // Make user deposit to fund a prelaunch minipool let refundAmount = '16'.ether; await userDeposit({ from: random, value: refundAmount }); // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake * 7n; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); // Create minipool prelaunchMinipool = await createMinipool({ from: node, value: '16'.ether }, minipoolSalt); }); // // General // it(printTitle('node', 'cannot stake a prelaunch pool if scrub period has not elapsed'), async () => { await shouldRevert(stakeMinipool(prelaunchMinipool, { from: node }), 'Was able to stake minipool before scrub period elapsed', 'Not enough time has passed to stake'); }); it(printTitle('node', 'can stake a prelaunch pool if scrub period has elapsed'), async () => { // Increase time by scrub period await helpers.time.increase(scrubPeriod + 1); // Should be able to stake await stakeMinipool(prelaunchMinipool, { from: node }); }); // // ODAO // it(printTitle('trusted node', 'can scrub a prelaunch minipool (no penalty)'), async () => { // 2 out of 3 should dissolve the minipool await voteScrub(prelaunchMinipool, { from: trustedNode1 }); await voteScrub(prelaunchMinipool, { from: trustedNode2 }); }); it(printTitle('trusted node', 'can scrub a prelaunch minipool (with penalty)'), async () => { // Enabled penalty await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.penalty.enabled', true, { from: owner }); // 2 out of 3 should dissolve the minipool await voteScrub(prelaunchMinipool, { from: trustedNode1 }); await voteScrub(prelaunchMinipool, { from: trustedNode2 }); }); it(printTitle('trusted node', 'cannot vote to scrub twice'), async () => { await voteScrub(prelaunchMinipool, { from: trustedNode1 }); await shouldRevert(voteScrub(prelaunchMinipool, { from: trustedNode1 }), 'Was able to vote scrub twice from same member', 'Member has already voted to scrub'); }); it(printTitle('trust node', 'cannot vote to scrub a staking minipool'), async () => { // Increase time by scrub period and stake await helpers.time.increase(scrubPeriod + 1); await stakeMinipool(prelaunchMinipool, { from: node }); // Should not be able to vote scrub await shouldRevert(voteScrub(prelaunchMinipool, { from: trustedNode1 }), 'Was able to vote scrub a staking minipool', 'The minipool can only be scrubbed while in prelaunch'); }); // // Misc // it(printTitle('guardian', 'can not set launch timeout lower than scrub period'), async () => { await shouldRevert(setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', scrubPeriod - 1, { from: owner }), 'Set launch timeout lower than scrub period', 'Launch timeout must be greater than scrub period'); }); it(printTitle('guardian', 'can not set scrub period higher than launch timeout'), async () => { await shouldRevert(setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', launchTimeout + 1, { from: owner }), 'Set scrub period higher than launch timeout', 'Scrub period must be less than launch timeout'); }); describe('With Scrubbed Minipool', () => { before(async () => { await voteScrub(prelaunchMinipool, { from: trustedNode1 }); await voteScrub(prelaunchMinipool, { from: trustedNode2 }); }); it(printTitle('node', 'can close a scrubbed minipool before funds are returned'), async () => { await close(prelaunchMinipool, { from: node }); }); it(printTitle('node', 'can close a scrubbed minipool after funds are returned'), async () => { // Send 16 ETH to minipool await random.sendTransaction({ to: prelaunchMinipool.target, value: '16'.ether, }); await close(prelaunchMinipool, { from: node }); }); it(printTitle('node', 'cannot close a scrubbed minipool twice'), async () => { await close(prelaunchMinipool, { from: node }); await shouldRevert(close(prelaunchMinipool, { from: node }), 'Was able to close twice', 'Minipool already closed'); }); it(printTitle('node', 'can not create a minipool at the same address after closing'), async () => { // Send 16 ETH to minipool await random.sendTransaction({ to: prelaunchMinipool.target, value: '16'.ether, }); await close(prelaunchMinipool, { from: node }); // Try to create the pool again await shouldRevert(createMinipool({ from: node, value: '16'.ether, }, minipoolSalt), 'Was able to recreate minipool at same address', 'Minipool already exists or was previously destroyed'); }); }); }); } ================================================ FILE: test/minipool/minipool-status-tests.js ================================================ import { before, describe } from 'mocha'; import { userDeposit } from '../_helpers/deposit'; import { createMinipool, getMinipoolMinimumRPLStake, minipoolStates, stakeMinipool } from '../_helpers/minipool'; import { nodeStakeRPL, registerNode, setNodeTrusted } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { RocketDAONodeTrustedProposals, RocketDAONodeTrustedSettingsMinipool, RocketDAONodeTrustedSettingsProposals, } from '../_utils/artifacts'; import { daoNodeTrustedExecute, daoNodeTrustedMemberLeave, daoNodeTrustedPropose, daoNodeTrustedVote, } from '../dao/scenario-dao-node-trusted'; import { getDAOProposalEndTime, getDAOProposalStartTime } from '../dao/scenario-dao-proposal'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketMinipoolStatus', () => { let owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, staker, random; // Constants let proposalCooldown = 10; let proposalVoteBlocks = 10; let scrubPeriod = (60 * 60 * 24); // 24 hours // Setup let stakingMinipool1; let stakingMinipool2; let stakingMinipool3; before(async () => { await globalSnapShot(); [ owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, staker, random, ] = await ethers.getSigners(); // Register node await registerNode({ from: node }); // Register trusted nodes await registerNode({ from: trustedNode1 }); await registerNode({ from: trustedNode2 }); await registerNode({ from: trustedNode3 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node1@home.com', owner); await setNodeTrusted(trustedNode2, 'saas_2', 'node2@home.com', owner); await setNodeTrusted(trustedNode3, 'saas_3', 'node3@home.com', owner); // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake.mul('3'.BN); await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); // Create minipools stakingMinipool1 = await createMinipool({ from: node, value: '16'.ether }); stakingMinipool2 = await createMinipool({ from: node, value: '16'.ether }); stakingMinipool3 = await createMinipool({ from: node, value: '16'.ether }); // Make and assign deposits to minipools await userDeposit({ from: staker, value: '16'.ether }); await userDeposit({ from: staker, value: '16'.ether }); await userDeposit({ from: staker, value: '16'.ether }); // Wait required scrub period await helpers.time.increase(scrubPeriod + 1); // Stake minipools await stakeMinipool(stakingMinipool1, { from: node }); await stakeMinipool(stakingMinipool2, { from: node }); await stakeMinipool(stakingMinipool3, { from: node }); // Check minipool statuses let stakingStatus1 = await stakingMinipool1.getStatus.call(); let stakingStatus2 = await stakingMinipool2.getStatus.call(); let stakingStatus3 = await stakingMinipool3.getStatus.call(); assertBN.equal(stakingStatus1, minipoolStates.Staking, 'Incorrect staking minipool status'); assertBN.equal(stakingStatus2, minipoolStates.Staking, 'Incorrect staking minipool status'); assertBN.equal(stakingStatus3, minipoolStates.Staking, 'Incorrect staking minipool status'); // Set a small proposal cooldown await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown', proposalCooldown, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.blocks', proposalVoteBlocks, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, { from: owner }); // Set a small vote delay await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.delay.blocks', 4, { from: owner }); }); async function trustedNode4JoinDao() { await registerNode({ from: trustedNode4 }); await setNodeTrusted(trustedNode4, 'saas_4', 'node@home.com', owner); } async function trustedNode4LeaveDao() { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Wait enough time to do a new proposal await helpers.mine(proposalCooldown); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [trustedNode4.address]); // Add the proposal let proposalId = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: trustedNode4, }); // Current block let timeCurrent = await helpers.time.latest(); // Now mine blocks until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalId) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalId, true, { from: trustedNode1 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode2 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode3 }); // Fast-forward to this voting period finishing timeCurrent = await helpers.time.latest(); await helpers.time.increase((await getDAOProposalEndTime(proposalId) - timeCurrent) + 1); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalId, { from: trustedNode1 }); // Member can now leave and collect any RPL bond await daoNodeTrustedMemberLeave(trustedNode4, { from: trustedNode4 }); } }); } ================================================ FILE: test/minipool/minipool-tests.js ================================================ import { before, describe, it } from 'mocha'; import { artifacts, RevertOnTransfer, RocketDAONodeTrustedSettingsMinipool, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketDAOProtocolSettingsNode, RocketDAOProtocolSettingsRewards, RocketMinipoolBase, RocketMinipoolBondReducer, RocketMinipoolDelegate, RocketMinipoolManager, RocketNodeManager, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { userDeposit } from '../_helpers/deposit'; import { createMinipool, dissolveMinipool, getMinipoolMinimumRPLStake, getNodeActiveMinipoolCount, minipoolStates, promoteMinipool, stakeMinipool, } from '../_helpers/minipool'; import { getNodeAverageFee, nodeStakeRPL, registerNode, setNodeTrusted, setNodeWithdrawalAddress, } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { close } from './scenario-close'; import { dissolve } from './scenario-dissolve'; import { refund } from './scenario-refund'; import { stake } from './scenario-stake'; import { beginUserDistribute, withdrawValidatorBalance } from './scenario-withdraw-validator-balance'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { setDAONodeTrustedBootstrapSetting, setDaoNodeTrustedBootstrapUpgrade, } from '../dao/scenario-dao-node-trusted-bootstrap'; import { reduceBond } from './scenario-reduce-bond'; import { assertBN } from '../_helpers/bn'; import { skimRewards } from './scenario-skim-rewards'; import { globalSnapShot } from '../_utils/snapshotting'; import * as assert from 'assert'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketMinipool', () => { let owner, node, emptyNode, nodeWithdrawalAddress, trustedNode, dummySwc, random; // Setup const secondsPerEpoch = 384; const launchTimeout = (60 * 60 * 72); // 72 hours const withdrawalDelay = 20; const scrubPeriod = (60 * 60 * 24); // 24 hours const bondReductionWindowStart = (2 * 24 * 60 * 60); const bondReductionWindowLength = (2 * 24 * 60 * 60); const rewardClaimBalanceIntervals = 28; const balanceSubmissionFrequency = (60 * 60 * 24); const rewardClaimPeriodTime = (rewardClaimBalanceIntervals * balanceSubmissionFrequency * secondsPerEpoch); // 28 days const userDistributeTime = (90 * 24 * 60 * 60); // 90 days const withdrawalBalance = '36'.ether; const lebDepositNodeAmount = '8'.ether; const halfDepositNodeAmount = '16'.ether; let newDelegateAddress = '0x0000000000000000000000000000000000000001'; let initialisedMinipool; let prelaunchMinipool; let prelaunchMinipool2; let stakingMinipool; let dissolvedMinipool; let oldDelegateAddress; let rocketMinipoolBondReducer; before(async () => { await globalSnapShot(); [ owner, node, emptyNode, nodeWithdrawalAddress, trustedNode, dummySwc, random, ] = await ethers.getSigners(); oldDelegateAddress = (await RocketMinipoolDelegate.deployed()).target; // Register node & set withdrawal address await registerNode({ from: node }); await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, { from: node }); // Register empty node await registerNode({ from: emptyNode }); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.withdrawal.delay', withdrawalDelay, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.bond.reduction.window.start', bondReductionWindowStart, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.bond.reduction.window.length', bondReductionWindowLength, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.balances.frequency', balanceSubmissionFrequency, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsRewards, 'rewards.claimsperiods', rewardClaimBalanceIntervals, { from: owner }); // Set rETH collateralisation target to a value high enough it won't cause excess ETH to be funneled back into deposit pool and mess with our calcs await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '50'.ether, { from: owner }); // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake * 7n; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); // Create a dissolved minipool await userDeposit({ from: random, value: '16'.ether }); dissolvedMinipool = await createMinipool({ from: node, value: '16'.ether }); await helpers.time.increase(launchTimeout + 1); await dissolveMinipool(dissolvedMinipool, { from: node }); // Create minipools await userDeposit({ from: random, value: '46'.ether }); prelaunchMinipool = await createMinipool({ from: node, value: '16'.ether }); prelaunchMinipool2 = await createMinipool({ from: node, value: '16'.ether }); stakingMinipool = await createMinipool({ from: node, value: '16'.ether }); initialisedMinipool = await createMinipool({ from: node, value: '16'.ether }); // Wait required scrub period await helpers.time.increase(scrubPeriod + 1); // Progress minipools into desired statuses await stakeMinipool(stakingMinipool, { from: node }); // Check minipool statuses let initialisedStatus = await initialisedMinipool.getStatus(); let prelaunchStatus = await prelaunchMinipool.getStatus(); let prelaunch2Status = await prelaunchMinipool2.getStatus(); let stakingStatus = await stakingMinipool.getStatus(); let dissolvedStatus = await dissolvedMinipool.getStatus(); assertBN.equal(initialisedStatus, minipoolStates.Initialised, 'Incorrect initialised minipool status'); assertBN.equal(prelaunchStatus, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status'); assertBN.equal(prelaunch2Status, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status'); assertBN.equal(stakingStatus, minipoolStates.Staking, 'Incorrect staking minipool status'); assertBN.equal(dissolvedStatus, minipoolStates.Dissolved, 'Incorrect dissolved minipool status'); rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed(); }); async function upgradeNetworkDelegateContract() { // Upgrade the delegate contract await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], newDelegateAddress, { from: owner, }); // Check effective delegate is still the original const minipool = await RocketMinipoolBase.at(stakingMinipool.target); const effectiveDelegate = await minipool.getEffectiveDelegate(); assert.notEqual(effectiveDelegate, newDelegateAddress, 'Effective delegate was updated'); } async function resetNetworkDelegateContract() { // Upgrade the delegate contract await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], oldDelegateAddress, { from: owner, }); } // // General // it(printTitle('random address', 'cannot send ETH to non-payable minipool delegate methods'), async () => { // Attempt to send ETH to view method await shouldRevert(prelaunchMinipool.getStatus({ from: random, value: '1'.ether, }), 'Sent ETH to a non-payable minipool delegate view method'); // Attempt to send ETH to mutator method await shouldRevert(refund(prelaunchMinipool, { from: node, value: '1'.ether, }), 'Sent ETH to a non-payable minipool delegate mutator method'); }); it(printTitle('minipool', 'has correct withdrawal credentials'), async () => { // Get contracts const rocketMinipoolManager = await RocketMinipoolManager.deployed(); // Withdrawal credentials settings const withdrawalPrefix = '01'; const padding = '0000000000000000000000'; // Get minipool withdrawal credentials let withdrawalCredentials = await rocketMinipoolManager.getMinipoolWithdrawalCredentials(initialisedMinipool.target); // Check withdrawal credentials let expectedWithdrawalCredentials = ('0x' + withdrawalPrefix + padding + initialisedMinipool.target.substr(2)); assert.equal(withdrawalCredentials.toLowerCase(), expectedWithdrawalCredentials.toLowerCase(), 'Invalid minipool withdrawal credentials'); }); it(printTitle('node operator', 'cannot create a minipool if network capacity is reached and destroying a minipool reduces the capacity'), async () => { // Retrieve the current number of minipools const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const minipoolCount = Number(await rocketMinipoolManager.getMinipoolCount()); // Set max to the current number await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.maximum.count', minipoolCount, { from: owner }); // Creating minipool should fail now await shouldRevert(createMinipool({ from: node, value: '16'.ether, }), 'Was able to create a minipool when capacity is reached', 'Global minipool limit reached'); // Destroy a pool await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress, true); // Creating minipool should no longer fail await createMinipool({ from: node, value: '16'.ether }); }); it(printTitle('node operator', 'cannot create a minipool if delegate address is set to a non-contract'), async () => { // Upgrade network delegate contract to random address await upgradeNetworkDelegateContract(); // Creating minipool should fail now await shouldRevert(createMinipool({ from: node, value: '16'.ether, }), 'Was able to create a minipool with bad delegate address', 'Delegate contract does not exist'); }); it(printTitle('node operator', 'cannot delegatecall to a delgate address that is a non-contract'), async () => { // Creating minipool should fail now let newMinipool = await createMinipool({ from: node, value: '16'.ether }); const newMinipoolBase = await RocketMinipoolBase.at(newMinipool.target); // Upgrade network delegate contract to random address await upgradeNetworkDelegateContract(); // Call upgrade delegate await newMinipoolBase.connect(node).setUseLatestDelegate(true, { from: node }); // Staking should fail now await shouldRevert(stakeMinipool(newMinipool, { from: node }), 'Was able to create a minipool with bad delegate address', 'Delegate contract does not exist'); // Reset the delegate to working contract to prevent invariant tests from failing await resetNetworkDelegateContract(); }); // // Finalise // it(printTitle('node operator', 'can finalise a user withdrawn minipool'), async () => { // Send enough ETH to allow distribution await owner.sendTransaction({ to: stakingMinipool.target, value: withdrawalBalance, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Withdraw without finalising await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, random); // Get number of active minipools before const count1 = await getNodeActiveMinipoolCount(node); // Finalise await stakingMinipool.connect(nodeWithdrawalAddress).finalise({ from: nodeWithdrawalAddress }); // Get number of active minipools after const count2 = await getNodeActiveMinipoolCount(node); // Make sure active minipool count reduced by one assertBN.equal(count1 - count2, 1, 'Active minipools did not decrement by 1'); }); it.only(printTitle('node operator', 'cannot finalise a withdrawn minipool twice'), async () => { // Send enough ETH to allow distribution await owner.sendTransaction({ to: stakingMinipool.target, value: withdrawalBalance, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Wait 2 days await helpers.time.increase(60 * 60 * 24 * 2 + 1); await beginUserDistribute(stakingMinipool, { from: random }); }); it(printTitle('node operator', 'cannot finalise a non-withdrawn minipool'), async () => { // Finalise await shouldRevert(stakingMinipool.connect(nodeWithdrawalAddress).finalise({ from: nodeWithdrawalAddress }), 'Minipool was finalised before withdrawn', 'Can only manually finalise after user distribution'); }); it(printTitle('random address', 'cannot finalise a withdrawn minipool'), async () => { // Withdraw without finalising await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress); // Finalise await shouldRevert(stakingMinipool.connect(random).finalise({ from: random }), 'Minipool was finalised by random', 'Invalid minipool owner'); }); // // Slash // it(printTitle('random address', 'can slash node operator if withdrawal balance is less than 16 ETH'), async () => { // Stake the prelaunch minipool (it has 16 ETH user funds) await stakeMinipool(prelaunchMinipool, { from: node }); // Send enough ETH to allow distribution await owner.sendTransaction({ to: prelaunchMinipool.target, value: '8'.ether, }); // Begin user distribution process await beginUserDistribute(prelaunchMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(prelaunchMinipool, '0'.ether, random); // Call slash method await prelaunchMinipool.connect(random).slash({ from: random }); // Check slashed flag const slashed = await (await RocketMinipoolManager.deployed()).getMinipoolRPLSlashed(prelaunchMinipool.target); assert.equal(slashed, true, 'Slashed flag not set'); // Auction house should now have slashed 8 ETH worth of RPL (which is 800 RPL at starting price) const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); const balance = await rocketVault.balanceOfToken('rocketAuctionManager', rocketTokenRPL.target); assertBN.equal(balance, '800'.ether); }); it(printTitle('node operator', 'is slashed if withdraw is processed when balance is less than 16 ETH'), async () => { // Stake the prelaunch minipool (it has 16 ETH user funds) await stakeMinipool(prelaunchMinipool, { from: node }); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(prelaunchMinipool, '8'.ether, nodeWithdrawalAddress, true); // Check slashed flag const slashed = await (await RocketMinipoolManager.deployed()).getMinipoolRPLSlashed(prelaunchMinipool.target); assert.equal(slashed, true, 'Slashed flag not set'); // Auction house should now have slashed 8 ETH worth of RPL (which is 800 RPL at starting price) const rocketVault = await RocketVault.deployed(); const rocketTokenRPL = await RocketTokenRPL.deployed(); const balance = await rocketVault.balanceOfToken('rocketAuctionManager', rocketTokenRPL.target); assertBN.equal(balance, '800'.ether); }); // // Dissolve // it(printTitle('node operator', 'cannot dissolve their own staking minipools'), async () => { // Attempt to dissolve staking minipool await shouldRevert(dissolve(stakingMinipool, { from: node, }), 'Dissolved a staking minipool'); }); it(printTitle('random address', 'can dissolve a timed out minipool at prelaunch'), async () => { // Time prelaunch minipool out await helpers.time.increase(launchTimeout); // Dissolve prelaunch minipool await dissolve(prelaunchMinipool, { from: random, }); }); it(printTitle('random address', 'cannot dissolve a minipool which is not at prelaunch'), async () => { // Time prelaunch minipool out await helpers.time.increase(launchTimeout); // Attempt to dissolve initialised minipool await shouldRevert(dissolve(initialisedMinipool, { from: random, }), 'Random address dissolved a minipool which was not at prelaunch'); }); it(printTitle('random address', 'cannot dissolve a minipool which has not timed out'), async () => { // Attempt to dissolve prelaunch minipool await shouldRevert(dissolve(prelaunchMinipool, { from: random, }), 'Random address dissolved a minipool which has not timed out'); }); // // Stake // it(printTitle('node operator', 'can stake a minipool at prelaunch'), async () => { // Stake prelaunch minipool await stake(prelaunchMinipool, null, { from: node, }); }); it(printTitle('node operator', 'cannot stake a minipool which is not at prelaunch'), async () => { // Attempt to stake initialised minipool await shouldRevert(stake(initialisedMinipool, null, { from: node, }), 'Staked a minipool which was not at prelaunch'); }); it(printTitle('node operator', 'cannot stake a minipool with a reused validator pubkey'), async () => { // Load contracts const rocketMinipoolManager = await RocketMinipoolManager.deployed(); // Get minipool validator pubkey const validatorPubkey = await rocketMinipoolManager.getMinipoolPubkey(prelaunchMinipool.target); // Stake prelaunch minipool await stake(prelaunchMinipool, null, { from: node }); // Attempt to stake second prelaunch minipool with same pubkey await shouldRevert(stake(prelaunchMinipool2, null, { from: node, }, validatorPubkey), 'Staked a minipool with a reused validator pubkey'); }); it(printTitle('node operator', 'cannot stake a minipool with incorrect withdrawal credentials'), async () => { // Get withdrawal credentials let invalidWithdrawalCredentials = '0x1111111111111111111111111111111111111111111111111111111111111111'; // Attempt to stake prelaunch minipool await shouldRevert(stake(prelaunchMinipool, invalidWithdrawalCredentials, { from: node, }), 'Staked a minipool with incorrect withdrawal credentials'); }); it(printTitle('random address', 'cannot stake a minipool'), async () => { // Attempt to stake prelaunch minipool await shouldRevert(stake(prelaunchMinipool, null, { from: random, }), 'Random address staked a minipool'); }); // // Withdraw validator balance // it(printTitle('random', 'random address cannot withdraw and destroy a node operators minipool balance'), async () => { // Wait 14 days await helpers.time.increase(60 * 60 * 24 * 14 + 1); // Attempt to send validator balance await shouldRevert(withdrawValidatorBalance(stakingMinipool, withdrawalBalance, random, true), 'Random address withdrew validator balance from a node operators minipool', 'Only owner can distribute right now'); }); it(printTitle('random', 'random address can trigger a payout of withdrawal balance if balance is greater than 16 ETH'), async () => { // Send enough ETH to allow distribution await owner.sendTransaction({ to: stakingMinipool.target, value: '32'.ether, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(stakingMinipool, '0'.ether, random); }); it(printTitle('random', 'random address cannot trigger a payout of withdrawal balance if balance is less than 16 ETH'), async () => { // Attempt to send validator balance await shouldRevert(withdrawValidatorBalance(stakingMinipool, '15'.ether, random, false), 'Random address was able to execute withdraw on sub 16 ETH minipool', 'Only owner can distribute right now'); }); it(printTitle('node operator withdrawal address', 'can withdraw their ETH once it is received, then distribute ETH to the rETH contract / deposit pool and destroy the minipool'), async () => { // Send validator balance and withdraw await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, nodeWithdrawalAddress, true); }); it(printTitle('node operator account', 'can also withdraw their ETH once it is received, then distribute ETH to the rETH contract / deposit pool and destroy the minipool'), async () => { // Send validator balance and withdraw await withdrawValidatorBalance(stakingMinipool, withdrawalBalance, node, true); }); it(printTitle('malicious node operator', 'can not prevent a payout by using a reverting contract as withdraw address'), async () => { // Set the node's withdraw address to a reverting contract const revertOnTransfer = await RevertOnTransfer.deployed(); await setNodeWithdrawalAddress(node, revertOnTransfer.target, { from: nodeWithdrawalAddress }); // Wait 14 days await helpers.time.increase(60 * 60 * 24 * 14 + 1); // Send enough ETH to allow distribution await owner.sendTransaction({ to: stakingMinipool.target, value: withdrawalBalance, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(stakingMinipool, '0'.ether, random); }); it(printTitle('random address', 'can send validator balance to a withdrawable minipool in one transaction'), async () => { await random.sendTransaction({ to: stakingMinipool.target, value: withdrawalBalance, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(stakingMinipool, '0'.ether, random); }); it(printTitle('random address', 'can send validator balance to a withdrawable minipool across multiple transactions'), async () => { // Get tx amount (half of withdrawal balance) let amount1 = withdrawalBalance / 2n; let amount2 = withdrawalBalance - amount1; await random.sendTransaction({ to: stakingMinipool.target, value: amount1, }); await owner.sendTransaction({ to: stakingMinipool.target, value: amount2, }); // Begin user distribution process await beginUserDistribute(stakingMinipool, { from: random }); // Wait 14 days await helpers.time.increase(userDistributeTime + 1); // Post an 8 ETH balance which should result in 8 ETH worth of RPL slashing await withdrawValidatorBalance(stakingMinipool, '0'.ether, random); }); // // Skim rewards // it(printTitle('node operator', 'can skim rewards less than 8 ETH'), async () => { // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '1'.ether, }); // Skim rewards from node await skimRewards(stakingMinipool, { from: node }); }); it(printTitle('random user', 'can skim rewards less than 8 ETH'), async () => { // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '1'.ether, }); // Skim rewards from node await skimRewards(stakingMinipool, { from: random }); }); it(printTitle('random user', 'can skim rewards less than 8 ETH twice'), async () => { // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '1'.ether, }); // Skim rewards from random await skimRewards(stakingMinipool, { from: random }); // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '1'.ether, }); // Skim rewards from random await skimRewards(stakingMinipool, { from: random }); }); it(printTitle('random user + node operator', 'can skim rewards less than 8 ETH twice interchangeably'), async () => { // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '1.5'.ether, }); // Skim rewards from random await skimRewards(stakingMinipool, { from: random }); // Send 1 ETH to the minipool await owner.sendTransaction({ to: stakingMinipool.target, value: '2'.ether, }); // Skim rewards from node await skimRewards(stakingMinipool, { from: node }); }); // // Close // it(printTitle('node operator', 'can close a dissolved minipool'), async () => { // Send 16 ETH to minipool await random.sendTransaction({ to: dissolvedMinipool.target, value: '16'.ether, }); // Close dissolved minipool await close(dissolvedMinipool, { from: node, }); }); it(printTitle('node operator', 'cannot close a minipool which is not dissolved'), async () => { // Attempt to close staking minipool await shouldRevert(close(stakingMinipool, { from: node, }), 'Closed a minipool which was not dissolved', 'The minipool can only be closed while dissolved'); }); it(printTitle('random address', 'cannot close a dissolved minipool'), async () => { // Attempt to close dissolved minipool await shouldRevert(close(dissolvedMinipool, { from: random, }), 'Random address closed a minipool', 'Invalid minipool owner'); }); // // Delegate upgrades // it(printTitle('node operator', 'can upgrade and rollback their delegate contract'), async () => { await upgradeNetworkDelegateContract(); // Get contract const minipool = await RocketMinipoolBase.at(stakingMinipool.target); // Store original delegate let originalDelegate = await minipool.getEffectiveDelegate(); // Call upgrade delegate await minipool.connect(node).delegateUpgrade({ from: node }); // Check delegate settings let effectiveDelegate = await minipool.getEffectiveDelegate(); let previousDelegate = await minipool.getPreviousDelegate(); assert.strictEqual(effectiveDelegate, newDelegateAddress, 'Effective delegate was not updated'); assert.strictEqual(previousDelegate, originalDelegate, 'Previous delegate was not updated'); // Call upgrade rollback await minipool.connect(node).delegateRollback({ from: node }); // Check effective delegate effectiveDelegate = await minipool.getEffectiveDelegate(); assert.strictEqual(effectiveDelegate, originalDelegate, 'Effective delegate was not rolled back'); }); it(printTitle('node operator', 'can use latest delegate contract'), async () => { await upgradeNetworkDelegateContract(); // Get contract const minipool = await RocketMinipoolBase.at(stakingMinipool.target); // Store original delegate let originalDelegate = await minipool.getEffectiveDelegate(); // Call upgrade delegate await minipool.connect(node).setUseLatestDelegate(true, { from: node }); let useLatest = await minipool.getUseLatestDelegate(); assert.equal(useLatest, true, 'Use latest flag was not set'); // Check delegate settings let effectiveDelegate = await minipool.getEffectiveDelegate(); let currentDelegate = await minipool.getDelegate(); assert.strictEqual(effectiveDelegate, newDelegateAddress, 'Effective delegate was not updated'); assert.strictEqual(currentDelegate, originalDelegate, 'Current delegate was updated'); // Upgrade the delegate contract again newDelegateAddress = '0x0000000000000000000000000000000000000002'; await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketMinipoolDelegate', [], newDelegateAddress, { from: owner, }); // Check effective delegate effectiveDelegate = await minipool.getEffectiveDelegate(); assert.strictEqual(effectiveDelegate, newDelegateAddress, 'Effective delegate was not updated'); // Reset the delegate to working contract to prevent invariant tests from failing await resetNetworkDelegateContract(); }); it(printTitle('random', 'cannot upgrade, rollback or set use latest delegate contract'), async () => { await upgradeNetworkDelegateContract(); // Get contract const minipool = await RocketMinipoolBase.at(stakingMinipool.target); // Call upgrade delegate from random await shouldRevert(minipool.connect(random).delegateUpgrade({ from: random }), 'Random was able to upgrade delegate', 'Only the node operator can access this method'); // Call upgrade delegate from node await minipool.connect(node).delegateUpgrade({ from: node }); // Call upgrade rollback from random await shouldRevert(minipool.connect(random).delegateRollback({ from: random }), 'Random was able to rollback delegate', 'Only the node operator can access this method'); // Call set use latest from random await shouldRevert(minipool.connect(random).setUseLatestDelegate(true, { from: random }), 'Random was able to set use latest delegate', 'Only the node operator can access this method'); // Reset the delegate to working contract to prevent invariant tests from failing await resetNetworkDelegateContract(); await minipool.connect(node).delegateUpgrade({ from: node }); }); // // Reducing bond amount // it(printTitle('node operator', 'can reduce bond amount to a valid deposit amount'), async () => { // Get contracts // Signal wanting to reduce await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + 1); // Reduction from 16 ETH to 8 ETH should be valid await reduceBond(stakingMinipool, { from: node }); }); it(printTitle('node operator', 'average node fee gets updated correctly on bond reduction'), async () => { // Get contracts const rocketNodeManager = await RocketNodeManager.deployed(); // Set the network node fee to 20% await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.20'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.20'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.20'.ether, { from: owner }); // Stake RPL to cover a 16 ETH and an 8 ETH minipool (1.6 + 2.4) let rplStake = '400'.ether; await mintRPL(owner, emptyNode, rplStake); await nodeStakeRPL(rplStake, { from: emptyNode }); // Deposit enough user funds to cover minipool creation await userDeposit({ from: random, value: '64'.ether }); // Create the minipools let minipool1 = await createMinipool({ from: emptyNode, value: '16'.ether }); let minipool2 = await createMinipool({ from: emptyNode, value: '16'.ether }); // Wait required scrub period await helpers.time.increase(scrubPeriod + 1); // Progress minipools into desired statuses await stakeMinipool(minipool1, { from: emptyNode }); await stakeMinipool(minipool2, { from: emptyNode }); // Set the network node fee to 10% await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.10'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.10'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.10'.ether, { from: owner }); // Signal wanting to reduce await rocketMinipoolBondReducer.connect(emptyNode).beginReduceBondAmount(minipool1.target, '8'.ether, { from: emptyNode }); await helpers.time.increase(bondReductionWindowStart + 1); // Reduction from 16 ETH to 8 ETH should be valid let fee1 = await rocketNodeManager.getAverageNodeFee(emptyNode); await reduceBond(minipool1, { from: emptyNode }); let fee2 = await rocketNodeManager.getAverageNodeFee(emptyNode); /* Node operator now has 1x 16 ETH bonded minipool at 20% node fee and 1x 8 ETH bonded minipool at 10% fee Before bond reduction average node fee should be 20%, weighted average node fee after should be 14% */ assertBN.equal(fee1, '0.20'.ether, 'Incorrect node fee'); assertBN.equal(fee2, '0.14'.ether, 'Incorrect node fee'); }); it(printTitle('node operator', 'can reduce bond amount to a valid deposit amount after reward period'), async () => { // Upgrade RocketNodeDeposit to add 4 ETH LEB support const RocketNodeDepositLEB4 = artifacts.require('RocketNodeDepositLEB4'); const rocketNodeDepositLEB4 = await RocketNodeDepositLEB4.deployed(); await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeDeposit', RocketNodeDepositLEB4.abi, rocketNodeDepositLEB4.target, { from: owner }); // Signal wanting to reduce await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + 1); // Reduction from 16 ETH to 8 ETH should be valid await reduceBond(stakingMinipool, { from: node }); // Increase await helpers.time.increase(rewardClaimPeriodTime + 1); // Signal wanting to reduce again await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '4'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + 1); // Reduction from 16 ETH to 8 ETH should be valid await reduceBond(stakingMinipool, { from: node }); }); it(printTitle('node operator', 'can not reduce bond amount to a valid deposit amount within reward period'), async () => { // Upgrade RocketNodeDeposit to add 4 ETH LEB support const RocketNodeDepositLEB4 = artifacts.require('RocketNodeDepositLEB4'); const rocketNodeDepositLEB4 = await RocketNodeDepositLEB4.deployed(); await setDaoNodeTrustedBootstrapUpgrade('upgradeContract', 'rocketNodeDeposit', RocketNodeDepositLEB4.abi, rocketNodeDepositLEB4.target, { from: owner }); // Signal wanting to reduce await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + 1); // Reduction from 16 ETH to 8 ETH should be valid await reduceBond(stakingMinipool, { from: node }); // Signal wanting to reduce again await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '4'.ether, { from: node }), 'Was able to reduce without waiting', 'Not enough time has passed since last bond reduction'); }); it(printTitle('node operator', 'cannot reduce bond without waiting'), async () => { // Signal wanting to reduce and wait 7 days await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); // Reduction from 16 ETH to 8 ETH should be valid await shouldRevert(reduceBond(stakingMinipool, { from: node }), 'Was able to reduce bond without waiting', 'Wait period not satisfied'); }); it(printTitle('node operator', 'cannot begin to reduce bond after odao has cancelled'), async () => { // Vote to cancel await rocketMinipoolBondReducer.connect(trustedNode).voteCancelReduction(stakingMinipool.target, { from: trustedNode }); // Signal wanting to reduce and wait 7 days await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }), 'Was able to begin to reduce bond', 'This minipool is not allowed to reduce bond'); }); it(printTitle('node operator', 'cannot reduce bond after odao has cancelled'), async () => { // Signal wanting to reduce and wait 7 days await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + 1); // Vote to cancel await rocketMinipoolBondReducer.connect(trustedNode).voteCancelReduction(stakingMinipool.target, { from: trustedNode }); // Wait and try to reduce await shouldRevert(reduceBond(stakingMinipool, { from: node }), 'Was able to reduce bond after it was cancelled', 'This minipool is not allowed to reduce bond'); }); it(printTitle('node operator', 'cannot reduce bond if wait period exceeds the limit'), async () => { // Signal wanting to reduce and wait 7 days await rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '8'.ether, { from: node }); await helpers.time.increase(bondReductionWindowStart + bondReductionWindowLength + 1); // Reduction from 16 ETH to 8 ETH should be valid await shouldRevert(reduceBond(stakingMinipool, { from: node }), 'Was able to reduce bond without waiting', 'Wait period not satisfied'); }); it(printTitle('node operator', 'cannot reduce bond without beginning the process first'), async () => { // Reduction from 16 ETH to 8 ETH should be valid await shouldRevert(reduceBond(stakingMinipool, { from: node }), 'Was able to reduce bond without beginning the process', 'Wait period not satisfied'); }); it(printTitle('node operator', 'cannot reduce bond amount to an invalid deposit amount'), async () => { // Reduce to 9 ether bond should fail await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '9'.ether, { from: node }), 'Was able to reduce to invalid bond', 'Invalid bond amount'); }); it(printTitle('node operator', 'cannot increase bond amount'), async () => { // Signal wanting to reduce and wait 7 days await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(stakingMinipool.target, '18'.ether, { from: node }), 'Was able to increase bond', 'Invalid bond amount'); }); it(printTitle('node operator', 'cannot reduce bond amount while in invalid state'), async () => { // Signal wanting to reduce and wait 7 days await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(prelaunchMinipool.target, '8'.ether, { from: node }), 'Was able to begin reducing bond on a prelaunch minipool', 'Minipool must be staking'); await shouldRevert(rocketMinipoolBondReducer.connect(node).beginReduceBondAmount(initialisedMinipool.target, '8'.ether, { from: node }), 'Was able to reduce bond on an initialised minipool', 'Minipool must be staking'); await helpers.time.increase(bondReductionWindowStart + 1); }); // // Zero min stake // it(printTitle('node operator', 'can create minipools when minimum stake is set to zero'), async () => { // Set min stake to 0 await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.per.minipool.stake.minimum', 0, { from: owner }); // Create multiple minipools from a new node with 0 RPL staked for (let i = 0; i < 5; i++) { await createMinipool({ from: emptyNode, value: '8'.ether }); } }); // // Misc checks // it(printTitle('node operator', 'cannot promote a non-vacant minipool'), async () => { // Try to promote (and fail) await shouldRevert(promoteMinipool(prelaunchMinipool, { from: node }), 'Was able to promote non-vacant minipool', 'Cannot promote a non-vacant minipool'); await shouldRevert(promoteMinipool(stakingMinipool, { from: node }), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch'); await shouldRevert(promoteMinipool(initialisedMinipool, { from: node }), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch'); await shouldRevert(promoteMinipool(dissolvedMinipool, { from: node }), 'Was able to promote non-vacant minipool', 'The minipool can only promote while in prelaunch'); }); const average_fee_tests = [ [ { fee: '0.10', amount: lebDepositNodeAmount, expectedFee: '0.10', }, { fee: '0.10', amount: lebDepositNodeAmount, expectedFee: '0.10', }, { fee: '0.10', amount: halfDepositNodeAmount, expectedFee: '0.10', }, ], [ { fee: '0.10', amount: halfDepositNodeAmount, expectedFee: '0.10', }, { fee: '0.20', amount: lebDepositNodeAmount, expectedFee: '0.16', }, { fee: '0.20', amount: lebDepositNodeAmount, expectedFee: '0.175', }, ], ]; for (let i = 0; i < average_fee_tests.length; i++) { let test = average_fee_tests[i]; it(printTitle('node operator', 'has correct average node fee #' + (i + 1)), async () => { async function setNetworkNodeFee(fee) { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', fee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', fee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', fee, { from: owner }); } // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake * 10n; await mintRPL(owner, emptyNode, rplStake); await nodeStakeRPL(rplStake, { from: emptyNode }); for (const step of test) { // Set fee to 10% await setNetworkNodeFee(step.fee.ether); // Deposit let minipool = await createMinipool({ from: emptyNode, value: step.amount }); await userDeposit({ from: random, value: '32'.ether }); // Wait required scrub period await helpers.time.increase(scrubPeriod + 1); // Progress minipools into desired statuses await stakeMinipool(minipool, { from: emptyNode }); // Get average let average = await getNodeAverageFee(emptyNode); assertBN.equal(average, step.expectedFee.ether, 'Invalid average fee'); } }); } }); } ================================================ FILE: test/minipool/minipool-vacant-tests.js ================================================ import { before, describe, it } from 'mocha'; import { RocketDAONodeTrustedSettingsMinipool, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketNodeStaking, } from '../_utils/artifacts'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { closeMinipool, createVacantMinipool, getMinipoolMinimumRPLStake, minipoolStates, promoteMinipool, } from '../_helpers/minipool'; import { getNodeDepositCredit, nodeStakeRPL, registerNode, setNodeTrusted, setNodeWithdrawalAddress, } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { voteScrub } from './scenario-scrub'; import { assertBN } from '../_helpers/bn'; import { refund } from './scenario-refund'; import { getValidatorPubkey } from '../_utils/beacon'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketMinipool', () => { let owner, node, nodeWithdrawalAddress, trustedNode1, trustedNode2, dummySwc, random; // Setup let launchTimeout = (60 * 60 * 72); // 72 hours let withdrawalDelay = 20; let promotionScrubDelay = (60 * 60 * 24); // 24 hours let prelaunchMinipool16; let prelaunchMinipool8; let rocketNodeStaking; before(async () => { await globalSnapShot(); [ owner, node, nodeWithdrawalAddress, trustedNode1, trustedNode2, dummySwc, random, ] = await ethers.getSigners(); rocketNodeStaking = await RocketNodeStaking.deployed(); // Register node & set withdrawal address await registerNode({ from: node }); await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, { from: node }); // Register trusted node await registerNode({ from: trustedNode1 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); await registerNode({ from: trustedNode2 }); await setNodeTrusted(trustedNode2, 'saas_2', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.withdrawal.delay', withdrawalDelay, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.promotion.scrub.period', promotionScrubDelay, { from: owner }); // Set rETH collateralisation target to a value high enough it won't cause excess ETH to be funneled back into deposit pool and mess with our calcs await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '50'.ether, { from: owner }); // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake * 7n; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); prelaunchMinipool16 = await createVacantMinipool('16'.ether, { from: node }); prelaunchMinipool8 = await createVacantMinipool('8'.ether, { from: node }); let prelaunch16Status = await prelaunchMinipool16.getStatus(); let prelaunch8Status = await prelaunchMinipool8.getStatus(); assertBN.equal(prelaunch16Status, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status'); assertBN.equal(prelaunch8Status, minipoolStates.Prelaunch, 'Incorrect prelaunch minipool status'); // ETH borrowed for node should be 40 ETH (24 + 16) assertBN.equal(await rocketNodeStaking.getNodeETHBorrowed(node), '40'.ether, 'Incorrect ETH borrowed'); }); // // Node operator // it(printTitle('node operator', 'can promote a 16 ETH vacant minipool after scrub period has elapsed'), async () => { // Wait required scrub period await helpers.time.increase(promotionScrubDelay + 1); // Promote the minipool await promoteMinipool(prelaunchMinipool16, { from: node }); // Verify new status let stakingStatus = await prelaunchMinipool16.getStatus(); assertBN.equal(stakingStatus, minipoolStates.Staking, 'Incorrect staking minipool status'); // Verify deposit credit balance increased by 16 ETH let creditBalance = await getNodeDepositCredit(node); assertBN.equal(creditBalance, '16'.ether); }); it(printTitle('node operator', 'can promote an 8 ETH vacant minipool after scrub period has elapsed'), async () => { // Wait required scrub period await helpers.time.increase(promotionScrubDelay + 1); // Promote the minipool await promoteMinipool(prelaunchMinipool8, { from: node }); // Verify new status let stakingStatus = await prelaunchMinipool8.getStatus(); assertBN.equal(stakingStatus, minipoolStates.Staking, 'Incorrect staking minipool status'); // Verify deposit credit balance increased by 24 ETH let creditBalance = await getNodeDepositCredit(node); assertBN.equal(creditBalance, '24'.ether); }); it(printTitle('node operator', 'cannot promote a vacant minipool before scrub period has elapsed'), async () => { // Try to promote (and fail) await shouldRevert(promoteMinipool(prelaunchMinipool16, { from: node }), 'Was able to promote minipool during scrub period', 'Not enough time has passed to promote'); }); it(printTitle('node operator', 'can refund their pre-migration rewards'), async () => { // Create a vacant minipool with current balance of 33 let minipool = await createVacantMinipool('8'.ether, { from: node }, null, '33'.ether); // Wait required scrub period await helpers.time.increase(promotionScrubDelay + 1); // Promote the minipool await promoteMinipool(minipool, { from: node }); // Verify refund balance const refundBalance = await minipool.getNodeRefundBalance(); assertBN.equal(refundBalance, '1'.ether, 'Invalid refund balance'); // Simulate skim await owner.sendTransaction({ to: minipool.target, value: '1'.ether, }); // Try to refund await refund(minipool, { from: node }); }); it(printTitle('node operator', 'cannot call refund while vacant'), async () => { // Create a vacant minipool with current balance of 33 let minipool = await createVacantMinipool('8'.ether, { from: node }, null, '33'.ether); // Try to refund await shouldRevert(refund(minipool, { from: node }), 'Was able to refund', 'Vacant minipool cannot refund'); }); it(printTitle('node operator', 'can not create a vacant minipool with an existing pubkey'), async () => { // Create minipool with a pubkey const pubkey = getValidatorPubkey(); await createVacantMinipool('16'.ether, { from: node }, null, '32'.ether, pubkey); // Try to create a new vacant minipool using the same pubkey await shouldRevert(createVacantMinipool('16'.ether, { from: node }, null, '32'.ether, pubkey), 'Was able to reuse pubkey', 'Validator pubkey is in use'); }); // // ODAO // it(printTitle('trusted node', 'can scrub a prelaunch minipool (no penalty)'), async () => { // 2 out of 3 should dissolve the minipool await voteScrub(prelaunchMinipool16, { from: trustedNode1 }); await voteScrub(prelaunchMinipool16, { from: trustedNode2 }); // ETH borrowed should still be 40 ETH assertBN.equal(await rocketNodeStaking.getNodeETHBorrowed(node), '40'.ether, 'Incorrect ETH borrowed'); // After closing ETH borrowed should drop by 16 await closeMinipool(prelaunchMinipool16, { from: node }); assertBN.equal(await rocketNodeStaking.getNodeETHBorrowed(node), '24'.ether, 'Incorrect ETH borrowed'); // 2 out of 3 should dissolve the minipool await voteScrub(prelaunchMinipool8, { from: trustedNode1 }); await voteScrub(prelaunchMinipool8, { from: trustedNode2 }); await closeMinipool(prelaunchMinipool8, { from: node }); assertBN.equal(await rocketNodeStaking.getNodeETHBorrowed(node), '0'.ether, 'Incorrect ETH borrowed'); }); it(printTitle('trusted node', 'can scrub a prelaunch minipool (no penalty applied even with scrub penalty active)'), async () => { // Enabled penalty await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.penalty.enabled', true, { from: owner }); // 2 out of 3 should dissolve the minipool await voteScrub(prelaunchMinipool16, { from: trustedNode1 }); await voteScrub(prelaunchMinipool16, { from: trustedNode2 }); }); }); } ================================================ FILE: test/minipool/minipool-withdrawal-tests.js ================================================ import { before, describe, it } from 'mocha'; import { PenaltyTest, RocketDAONodeTrustedSettingsMinipool, RocketDAOProtocolSettingsMinipool, RocketDAOProtocolSettingsNetwork, RocketMinipoolPenalty, RocketNodeStaking, RocketStorage, } from '../_utils/artifacts'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { userDeposit } from '../_helpers/deposit'; import { createMinipool, getMinipoolMinimumRPLStake, stakeMinipool } from '../_helpers/minipool'; import { nodeStakeRPL, registerNode, setNodeTrusted, setNodeWithdrawalAddress } from '../_helpers/node'; import { mintRPL } from '../_helpers/tokens'; import { beginUserDistribute, withdrawValidatorBalance } from './scenario-withdraw-validator-balance'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { setDAONodeTrustedBootstrapSetting, setDaoNodeTrustedBootstrapUpgrade, } from '../dao/scenario-dao-node-trusted-bootstrap'; import { submitPrices } from '../_helpers/network'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketMinipool', () => { let owner, node, nodeWithdrawalAddress, trustedNode, random; let launchTimeout = (60 * 60 * 72); // 72 hours let withdrawalDelay = 20; let scrubPeriod = (60 * 60 * 24); // 24 hours let minipool; let maxPenaltyRate = '0.5'.ether; let penaltyTestContract; let userDistributeStartTime = (60 * 60 * 24 * 90); let userDistributeLength = (60 * 60); before(async () => { await globalSnapShot(); [ owner, node, nodeWithdrawalAddress, trustedNode, random, ] = await ethers.getSigners(); // Hard code fee to 10% const fee = '0.1'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', fee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', fee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', fee, { from: owner }); // Register node & set withdrawal address await registerNode({ from: node }); await setNodeWithdrawalAddress(node, nodeWithdrawalAddress, { from: node }); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.withdrawal.delay', withdrawalDelay, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.user.distribute.window.start', userDistributeStartTime, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.user.distribute.window.length', userDistributeLength, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, { from: owner }); // Set rETH collateralisation target to a value high enough it won't cause excess ETH to be funneled back into deposit pool and mess with our calcs await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '50'.ether, { from: owner }); // Set RPL price let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; await submitPrices(block, slotTimestamp, '1'.ether, { from: trustedNode }); // Add penalty helper contract const rocketStorage = await RocketStorage.deployed(); penaltyTestContract = await PenaltyTest.new(rocketStorage.target); await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketPenaltyTest', PenaltyTest.abi, penaltyTestContract.target, { from: owner, }); // Enable penalties const rocketMinipoolPenalty = await RocketMinipoolPenalty.deployed(); await rocketMinipoolPenalty.connect(owner).setMaxPenaltyRate(maxPenaltyRate); // Deposit some user funds to assign to pools let userDepositAmount = '16'.ether; await userDeposit({ from: random, value: userDepositAmount }); // Stake RPL to cover minipools let minipoolRplStake = await getMinipoolMinimumRPLStake(); let rplStake = minipoolRplStake * 3n; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); await mintRPL(owner, trustedNode, rplStake); await nodeStakeRPL(rplStake, { from: trustedNode }); // Create minipools minipool = await createMinipool({ from: node, value: '16'.ether }); // Wait required scrub period await helpers.time.increase(scrubPeriod + 1); // Stake minipools await stakeMinipool(minipool, { from: node }); }); async function withdrawAndCheck(minipool, withdrawalBalance, from, finalise, expectedUser, expectedNode, userDistribute = false) { const withdrawalBalanceBN = withdrawalBalance.ether; const expectedUserBN = expectedUser.ether; const expectedNodeBN = expectedNode.ether; let result; if (userDistribute) { // Send ETH to minipool await from.sendTransaction({ to: minipool.target, value: withdrawalBalanceBN, }); // Begin user distribution process await beginUserDistribute(minipool, { from }); // Wait 90 days await helpers.time.increase(userDistributeStartTime + 1); // Process withdrawal result = await withdrawValidatorBalance(minipool, '0'.ether, from, finalise); } else { // Process withdrawal result = await withdrawValidatorBalance(minipool, withdrawalBalanceBN, from, finalise); } // Check results assertBN.equal(expectedUserBN, result.rethBalanceChange, 'User balance was incorrect'); assertBN.equal(expectedNodeBN, result.nodeBalanceChange, 'Node balance was incorrect'); } async function slashAndCheck(from, expectedSlash) { // Get contracts const rocketNodeStaking = await RocketNodeStaking.deployed(); const rplStake1 = await rocketNodeStaking.getNodeRPLStake(node); await minipool.slash({ from: from }); const rplStake2 = await rocketNodeStaking.getNodeRPLStake(node); const slashedAmount = rplStake1 - rplStake2; assertBN.equal(expectedSlash, slashedAmount, 'Slashed amount was incorrect'); } it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 32 ETH and not marked as withdrawable'), async () => { // Process withdraw await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, false, '17.8', '18.2'); }); it(printTitle('random user', 'can process withdrawal when balance is greater than 32 ETH and not marked as withdrawable'), async () => { // Process withdraw await withdrawAndCheck(minipool, '36', random, false, '17.8', '18.2', true); }); it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 16 ETH and less than 32 ETH'), async () => { // Process withdraw await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, true, '16', '12'); }); it(printTitle('random user', 'can process withdrawal when balance is greater than 16 ETH and less than 32 ETH'), async () => { // Wait 14 days await helpers.time.increase(userDistributeStartTime + 1); // Process withdraw await withdrawAndCheck(minipool, '28', random, false, '16', '12', true); }); it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is greater than 16 ETH, less than 32 ETH and not marked as withdrawable'), async () => { // Process withdraw await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, false, '16', '12'); }); it(printTitle('random user', 'can process withdrawal when balance is greater than 16 ETH, less than 32 ETH and not marked as withdrawable'), async () => { // Process withdraw await withdrawAndCheck(minipool, '28', random, false, '16', '12', true); }); it(printTitle('random user', 'can not begin user distribution without waiting for window to pass'), async () => { // Send ETH to minipool await random.sendTransaction({ to: minipool.target, value: '32'.ether, }); await beginUserDistribute(minipool, { from: random }); await shouldRevert(beginUserDistribute(minipool, { from: random }), 'Was able to begin user distribution again', 'User distribution already pending'); }); it(printTitle('random user', 'can begin user distribution after window has passed'), async () => { // Send ETH to minipool await random.sendTransaction({ to: minipool.target, value: '32'.ether, }); await beginUserDistribute(minipool, { from: random }); await helpers.time.increase(userDistributeLength + userDistributeStartTime + 1); await beginUserDistribute(minipool, { from: random }); }); it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is less than 16 ETH'), async () => { // Process withdraw await withdrawAndCheck(minipool, '15', nodeWithdrawalAddress, true, '15', '0'); }); it(printTitle('random address', 'cannot slash a node operator by sending 4 ETH and distribute after 14 days'), async () => { // Process withdraw await withdrawAndCheck(minipool, '28', nodeWithdrawalAddress, true, '16', '12'); // Wait 14 days and mine enough blocks to pass cooldown await helpers.time.increase(60 * 60 * 24 * 14 + 1); await helpers.mine(101); // Process withdraw and attempt to slash await withdrawAndCheck(minipool, '8', random, false, '8', '0', true); await shouldRevert(minipool.connect(owner).slash(), 'Was able to slash minipool', 'No balance to slash'); }); it(printTitle('node operator withdrawal address', 'can process withdrawal when balance is less than 16 ETH'), async () => { // Process withdraw await withdrawAndCheck(minipool, '15', nodeWithdrawalAddress, false, '15', '0'); }); it(printTitle('node operator withdrawal address', 'should fail when trying to distribute rewards with greater than 8 ETH balance'), async () => { // Process withdraw await random.sendTransaction({ to: minipool.target, gas: 12450000, value: '8.001'.ether, }); await shouldRevert(minipool.connect(nodeWithdrawalAddress).distributeBalance(true), 'Distribute succeeded', 'Balance exceeds 8 ether'); }); // ETH penalty events it(printTitle('node operator withdrawal address', 'can process withdrawal and finalise pool when penalised by DAO'), async () => { // Penalise the minipool 50% of it's ETH await penaltyTestContract.connect(owner).setPenaltyRate(minipool.target, maxPenaltyRate); // Process withdraw - 36 ETH would normally give node operator 18.2 and user 17.8 but with a 50% penalty, and extra 9.1 goes to the user await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '26.9', '9.1'); }); it(printTitle('node operator withdrawal address', 'cannot be penalised greater than the max penalty rate set by DAO'), async () => { // Try to penalise the minipool 75% of it's ETH (max is 50%) await penaltyTestContract.connect(owner).setPenaltyRate(minipool.target, '0.75'.ether); // Process withdraw - 36 ETH would normally give node operator 19 and user 17 but with a 50% penalty, and extra 9.5 goes to the user await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '26.9', '9.1'); }); it(printTitle('guardian', 'can disable penalising all together'), async () => { // Disable penalising by setting rate to 0 const rocketMinipoolPenalty = await RocketMinipoolPenalty.deployed(); await rocketMinipoolPenalty.connect(owner).setMaxPenaltyRate('0'); // Try to penalise the minipool 50% await penaltyTestContract.setPenaltyRate(minipool.target, '0.5'.ether); // Process withdraw await withdrawAndCheck(minipool, '36', nodeWithdrawalAddress, true, '17.8', '18.2'); }); }); } ================================================ FILE: test/minipool/scenario-close.js ================================================ import { RocketNodeManager, RocketNodeStaking } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Close a minipool export async function close(minipool, txOptions) { // Load contracts const rocketNodeManager = await RocketNodeManager.deployed(); const rocketNodeStaking = await RocketNodeStaking.deployed(); // Get parameters let nodeAddress = await minipool.getNodeAddress(); let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Get initial node balance & minipool balances let [nodeBalance1, ethBorrowed1, minipoolBalance, userDepositBalance] = await Promise.all([ ethers.provider.getBalance(nodeWithdrawalAddress), rocketNodeStaking.getNodeETHBorrowed(txOptions.from), ethers.provider.getBalance(minipool.target), minipool.getUserDepositBalance(), ]); // Set gas price let gasPrice = '20'.gwei; txOptions.gasPrice = gasPrice; // Close & get tx fee let tx = await minipool.connect(txOptions.from).close(txOptions); let txReceipt = await tx.wait(); let txFee = gasPrice * txReceipt.gasUsed; // Get updated node balance & minipool contract code let [nodeBalance2, ethBorrowed2] = await Promise.all([ ethers.provider.getBalance(nodeWithdrawalAddress), rocketNodeStaking.getNodeETHBorrowed(txOptions.from), ]); // Check balances let expectedNodeBalance = nodeBalance1 + minipoolBalance; if (nodeWithdrawalAddress === nodeAddress) expectedNodeBalance = expectedNodeBalance - txFee; assertBN.equal(nodeBalance2, expectedNodeBalance, 'Incorrect updated node nETH balance'); // Expect node's ETH borrowed to be decreased by userDepositBalance assertBN.equal(ethBorrowed1 - ethBorrowed2, userDepositBalance, 'Incorrect ETH borrowed'); } ================================================ FILE: test/minipool/scenario-dissolve.js ================================================ // Dissolve a minipool import { minipoolStates } from '../_helpers/minipool'; import * as assert from 'assert'; export async function dissolve(minipool, txOptions) { // Get minipool details function getMinipoolDetails() { return Promise.all([ minipool.getStatus(), minipool.getUserDepositBalance(), ]).then( ([status, userDepositBalance]) => ({ status: Number(status), userDepositBalance }), ); } // Get initial minipool details let details1 = await getMinipoolDetails(); // Dissolve await minipool.connect(txOptions.from).dissolve(txOptions); // Get updated minipool details let details2 = await getMinipoolDetails(); // Check minipool details assert.notEqual(details1.status, minipoolStates.Dissolved, 'Incorrect initial minipool status'); assert.equal(details2.status, minipoolStates.Dissolved, 'Incorrect updated minipool status'); } ================================================ FILE: test/minipool/scenario-reduce-bond.js ================================================ import { RocketMinipoolBondReducer, RocketMinipoolManager, RocketNodeDeposit, RocketNodeStaking, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Reduce bonding amount of a minipool export async function reduceBond(minipool, txOptions = null) { const rocketNodeDeposit = await RocketNodeDeposit.deployed(); const rocketNodeStaking = await RocketNodeStaking.deployed(); const rocketMinipoolBondReducer = await RocketMinipoolBondReducer.deployed(); const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const node = await minipool.getNodeAddress(); const newBond = await rocketMinipoolBondReducer.getReduceBondValue(minipool.target); const prevBond = await minipool.getNodeDepositBalance(); // Get minipool balances function getMinipoolBalances() { return Promise.all([ minipool.getNodeDepositBalance(), minipool.getUserDepositBalance(), rocketNodeDeposit.getNodeDepositCredit(node), rocketNodeStaking.getNodeETHBorrowed(node), ]).then( ([nodeDepositBalance, userDepositBalance, nodeDepositCredit, ethBorrowed]) => ({ nodeDepositBalance, userDepositBalance, nodeDepositCredit, ethBorrowed }), ); } // Get node details function getNodeDetails() { return Promise.all([ rocketMinipoolManager.getNodeStakingMinipoolCountBySize(node, prevBond), rocketMinipoolManager.getNodeStakingMinipoolCountBySize(node, newBond), rocketMinipoolManager.getNodeStakingMinipoolCount(node), ]).then( ([prevBondCount, newBondCount, totalCount]) => ({ prevBondCount, newBondCount, totalCount }), ); } // Get new bond amount const amount = await rocketMinipoolBondReducer.getReduceBondValue(minipool.target); // Record balances before and after calling reduce bond function const balances1 = await getMinipoolBalances(); const details1 = await getNodeDetails(); await minipool.connect(txOptions.from).reduceBondAmount(txOptions); const balances2 = await getMinipoolBalances(); const details2 = await getNodeDetails(); // Verify results const delta = balances1.nodeDepositBalance - amount; assertBN.equal(balances2.nodeDepositBalance, delta); assertBN.equal(balances2.userDepositBalance - balances1.userDepositBalance, delta); assertBN.equal(balances2.nodeDepositCredit - balances1.nodeDepositCredit, delta); assertBN.equal(balances2.ethBorrowed - balances1.ethBorrowed, delta); // Overall number of minipools shouldn't change assertBN.equal(details2.totalCount, details1.totalCount); // Prev bond amount should decrement by 1 assertBN.equal(details1.prevBondCount - details2.prevBondCount, 1n); // New bond amount should increment by 1 assertBN.equal(details2.newBondCount - details1.newBondCount, 1n); } ================================================ FILE: test/minipool/scenario-refund.js ================================================ import { RocketNodeManager } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Refund refinanced node balance from a minipool export async function refund(minipool, txOptions) { // Load contracts const rocketNodeManager = await RocketNodeManager.deployed(); // Get parameters let nodeAddress = await minipool.getNodeAddress(); let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Get balances function getBalances() { return Promise.all([ minipool.getNodeRefundBalance(), ethers.provider.getBalance(minipool.target), ethers.provider.getBalance(nodeWithdrawalAddress), ]).then( ([nodeRefund, minipoolEth, nodeEth]) => ({ nodeRefund, minipoolEth, nodeEth }), ); } // Get initial balances let balances1 = await getBalances(); // Set gas price let gasPrice = '20'.gwei; txOptions.gasPrice = gasPrice; // Refund & get tx fee let tx = await minipool.connect(txOptions.from).refund(txOptions); let txReceipt = await tx.wait(); let txFee = gasPrice * txReceipt.gasUsed; // Get updated balances let balances2 = await getBalances(); // Check balances let expectedNodeBalance = balances1.nodeEth + balances1.nodeRefund; if (nodeWithdrawalAddress === nodeAddress) expectedNodeBalance = expectedNodeBalance - txFee; assertBN.isAbove(balances1.nodeRefund, '0'.ether, 'Incorrect initial node refund balance'); assertBN.equal(balances2.nodeRefund, '0'.ether, 'Incorrect updated node refund balance'); assertBN.equal(balances2.minipoolEth, balances1.minipoolEth - balances1.nodeRefund, 'Incorrect updated minipool ETH balance'); assertBN.equal(balances2.nodeEth, expectedNodeBalance, 'Incorrect updated node ETH balance'); } ================================================ FILE: test/minipool/scenario-scrub.js ================================================ import { RocketDAONodeTrusted, RocketDAONodeTrustedSettingsMinipool, RocketMinipoolManager, RocketNodeStaking, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { minipoolStates } from '../_helpers/minipool'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; export async function voteScrub(minipool, txOptions) { // Get minipool owner const nodeAddress = await minipool.getNodeAddress(); // Get contracts const rocketNodeStaking = await RocketNodeStaking.deployed(); const rocketDAONodeTrustedSettingsMinipool = await RocketDAONodeTrustedSettingsMinipool.deployed(); // Get minipool details function getMinipoolDetails() { return Promise.all([ minipool.getStatus(), minipool.getUserDepositBalance(), ethers.provider.getBalance(minipool.target), minipool.getTotalScrubVotes(), rocketNodeStaking.getNodeRPLStake(nodeAddress), rocketDAONodeTrustedSettingsMinipool.getScrubPenaltyEnabled(), minipool.getVacant(), ]).then( ([status, userDepositBalance, minipoolBalance, votes, nodeRPLStake, penaltyEnabled, vacant]) => ({ status: Number(status), userDepositBalance, minipoolBalance, votes, nodeRPLStake, penaltyEnabled, vacant, }), ); } // Get initial minipool details let details1 = await getMinipoolDetails(); // Dissolve await minipool.connect(txOptions.from).voteScrub(txOptions); // Get updated minipool details let details2 = await getMinipoolDetails(); // Get member count const rocketDAONodeTrusted = await RocketDAONodeTrusted.deployed(); const memberCount = await rocketDAONodeTrusted.getMemberCount(); const quorum = memberCount / 2n; // Check state if (details1.votes + 1n > quorum) { assert.equal(details2.status, minipoolStates.Dissolved, 'Incorrect updated minipool status'); // Check if vacant if (!details1.vacant) { // Check slashing if penalties are enabled if (details1.penaltyEnabled) { // Check user deposit balance + 2.4 eth penalty left the minipool const minipoolBalanceDiff = details2.minipoolBalance - details1.minipoolBalance; assertBN.equal(minipoolBalanceDiff, -(details1.userDepositBalance + '2.4'.ether), 'User balance is incorrect'); } else { // Check user deposit balance left the minipool const minipoolBalanceDiff = details2.minipoolBalance - details1.minipoolBalance; assertBN.equal(minipoolBalanceDiff, -details1.userDepositBalance, 'User balance is incorrect'); } } else { // Expect no change in minipool balance const minipoolBalanceDiff = details2.minipoolBalance - details1.minipoolBalance; assertBN.equal(minipoolBalanceDiff, 0n, 'User balance is incorrect'); // Expect pubkey -> minipool mapping to be removed const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const actualPubKey = await rocketMinipoolManager.getMinipoolPubkey(minipool.target); const reverseAddress = await rocketMinipoolManager.getMinipoolByPubkey(actualPubKey); assert.equal(reverseAddress, '0x0000000000000000000000000000000000000000'); } } else { assertBN.equal(details2.votes - details1.votes, 1, 'Vote count not incremented'); assertBN.notEqual(details2.status, minipoolStates.Dissolved, 'Incorrect updated minipool status'); assertBN.equal(details2.nodeRPLStake, details1.nodeRPLStake, 'RPL was slashed'); } } ================================================ FILE: test/minipool/scenario-skim-rewards.js ================================================ import { RocketNodeManager, RocketTokenRETH } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; export async function skimRewards(minipool, txOptions) { // Load contracts const [ rocketTokenRETH, rocketNodeManager, ] = await Promise.all([ RocketTokenRETH.deployed(), RocketNodeManager.deployed(), ]); // Get parameters let [ nodeAddress, nodeFee, nodeCapital, userCapital, ] = await Promise.all([ minipool.getNodeAddress(), minipool.getNodeFee(), minipool.getNodeDepositBalance(), minipool.getUserDepositBalance(), ]); // Get node parameters let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Get balances function getBalances() { return Promise.all([ ethers.provider.getBalance(rocketTokenRETH.target), ethers.provider.getBalance(nodeWithdrawalAddress), ethers.provider.getBalance(minipool.target), minipool.getNodeRefundBalance(), ]).then( ([rethContractEth, nodeWithdrawalEth, minipoolEth, nodeRefundBalance]) => ({ rethContractEth, nodeWithdrawalEth, minipoolEth, nodeRefundBalance }), ); } // Get initial balances & withdrawal processed status const balances1 = await getBalances(); const realBalance = balances1.minipoolEth - balances1.nodeRefundBalance; assertBN.isBelow(realBalance, '8'.ether, 'Cannot skim rewards greater than 8 ETH'); // Set gas price txOptions.gasPrice = '20'.gwei; // Payout the balances now let tx = await minipool.connect(txOptions.from).distributeBalance(true, txOptions); let txReceipt = await tx.wait(); let txFee = txOptions.gasPrice * txReceipt.gasUsed; // Get updated balances & withdrawal processed status const balances2 = await getBalances(); // Add the fee back into the balance to make assertions easier if (txOptions.from === nodeWithdrawalAddress) { balances2.nodeWithdrawalEth = balances2.nodeWithdrawalEth + txFee; } // Calculate actual rewards const nodeBalanceChange = balances2.nodeWithdrawalEth - balances1.nodeWithdrawalEth; const nodeRefundBalanceChange = balances2.nodeRefundBalance - balances2.nodeRefundBalance; const rethBalanceChange = balances2.rethContractEth - balances1.rethContractEth; // Calculate expected rewards const rewards = balances1.minipoolEth - balances1.nodeRefundBalance; const nodePortion = rewards * nodeCapital / (userCapital + nodeCapital); const userPortion = rewards - nodePortion; const nodeRewards = nodePortion + (userPortion * nodeFee / '1'.ether); const userRewards = rewards - nodeRewards; // Check rETH balance has increased by expected amount assertBN.equal(rethBalanceChange, userRewards, 'Incorrect user rewards distributed'); if (txOptions.from === nodeWithdrawalAddress || txOptions.from === nodeAddress) { // When NO calls it should send the skimmed rewards + any accured in the refund balance to withdrawal address assertBN.equal(nodeBalanceChange, nodeRewards + balances1.nodeRefundBalance, 'Incorrect node rewards distributed'); } else { // When someone else calls it just accrues in refund balance assertBN.equal(nodeRefundBalanceChange, nodeRefundBalanceChange, 'Incorrect node rewards distributed'); } } ================================================ FILE: test/minipool/scenario-stake.js ================================================ import { RocketMinipoolManager } from '../_utils/artifacts'; import { getDepositDataRoot, getValidatorSignature } from '../_utils/beacon'; import { assertBN } from '../_helpers/bn'; import { minipoolStates } from '../_helpers/minipool'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Stake a minipool export async function stake(minipool, withdrawalCredentials, txOptions, validatorPubkey = null) { // Load contracts const [ rocketMinipoolManager, ] = await Promise.all([ RocketMinipoolManager.deployed(), ]); // Get minipool validator pubkey if (!validatorPubkey) validatorPubkey = await rocketMinipoolManager.getMinipoolPubkey(minipool.target); // Get minipool withdrawal credentials if (!withdrawalCredentials) withdrawalCredentials = await rocketMinipoolManager.getMinipoolWithdrawalCredentials(minipool.target); // Get validator deposit data let depositData = { pubkey: Buffer.from(validatorPubkey.substr(2), 'hex'), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(31000000000), // 31 ETH in gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); // Get minipool details function getMinipoolDetails() { return Promise.all([ minipool.getStatus(), ethers.provider.getBalance(minipool.target), ]).then( ([status, balance]) => ({ status: Number(status), balance }), ); } // Get initial minipool details & minipool by validator pubkey let [details1] = await Promise.all([ getMinipoolDetails(), ]); // Stake await minipool.connect(txOptions.from).stake(depositData.signature, depositDataRoot, txOptions); // Get updated minipool details & minipool by validator pubkey let [details2, validatorMinipool2] = await Promise.all([ getMinipoolDetails(), rocketMinipoolManager.getMinipoolByPubkey(validatorPubkey), ]); // Check minpool details assert.notEqual(details1.status, minipoolStates.Staking, 'Incorrect initial minipool status'); assert.equal(details2.status, minipoolStates.Staking, 'Incorrect updated minipool status'); assertBN.equal(details2.balance, details1.balance - '31'.ether, 'Incorrect updated minipool ETH balance'); // Check minipool by validator pubkey assert.strictEqual(validatorMinipool2, minipool.target, 'Incorrect updated minipool by validator pubkey'); } ================================================ FILE: test/minipool/scenario-withdraw-validator-balance.js ================================================ import { RocketDepositPool, RocketMinipoolPenalty, RocketNodeManager, RocketTokenRETH } from '../_utils/artifacts' import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; export async function withdrawValidatorBalance(minipool, withdrawalBalance, from) { // Load contracts const [ rocketDepositPool, rocketTokenRETH, rocketNodeManager ] = await Promise.all([ RocketDepositPool.deployed(), RocketTokenRETH.deployed(), RocketNodeManager.deployed(), ]); // Get node parameters let nodeAddress = await minipool.getNodeAddress(); let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Get parameters let [ nodeFee ] = await Promise.all([ minipool.getNodeFee(), ]); // Get balances function getBalances() { return Promise.all([ ethers.provider.getBalance(rocketTokenRETH.target), rocketDepositPool.getBalance(), ethers.provider.getBalance(nodeWithdrawalAddress), ethers.provider.getBalance(minipool.target), ]).then( ([rethContractEth, depositPoolEth, nodeWithdrawalEth, minipoolEth]) => ({rethContractEth, depositPoolEth, nodeWithdrawalEth, minipoolEth}) ); } // Get minipool balances function getMinipoolBalances() { return Promise.all([ minipool.getNodeDepositBalance(), minipool.getNodeRefundBalance(), minipool.getUserDepositBalance(), ]).then( ([nodeDepositBalance, nodeRefundBalance, userDepositBalance]) => ({nodeDepositBalance, nodeRefundBalance, userDepositBalance}) ); } // Send validator balance to minipool if (withdrawalBalance > 0n) { await from.sendTransaction({ to: minipool.target, gas: 12450000, value: withdrawalBalance }); } // Get total withdrawal balance withdrawalBalance = await ethers.provider.getBalance(minipool.target); // Get initial balances & withdrawal processed status let [balances1, minipoolBalances1] = await Promise.all([ getBalances(), getMinipoolBalances() ]); // Set gas price let gasPrice = '20'.gwei; // Payout the balances now let tx = await minipool.connect(from).distributeBalance(false, { from: from, gasPrice: gasPrice }); const txReceipt = await tx.wait(); let txFee = gasPrice * txReceipt.gasUsed; // Get updated balances & withdrawal processed status let [balances2, minipoolBalances2] = await Promise.all([ getBalances(), getMinipoolBalances() ]); // Add the fee back into the balance to make assertions easier if (from.address === nodeWithdrawalAddress) { balances2.nodeWithdrawalEth = balances2.nodeWithdrawalEth + txFee; } let nodeBalanceChange = balances2.nodeWithdrawalEth + minipoolBalances2.nodeRefundBalance - balances1.nodeWithdrawalEth + minipoolBalances1.nodeRefundBalance; let rethBalanceChange = balances2.rethContractEth - balances1.rethContractEth; let depositPoolChange = balances2.depositPoolEth - balances1.depositPoolEth; // Get penalty rate for this minipool const rocketMinipoolPenalty = await RocketMinipoolPenalty.deployed(); const penaltyRate = await rocketMinipoolPenalty.getPenaltyRate(minipool.target); // Calculate rewards let depositBalance = '32'.ether; if (withdrawalBalance >= depositBalance) { let depositType = await minipool.getDepositType(); let userAmount = minipoolBalances1.userDepositBalance; let rewards = withdrawalBalance - depositBalance; if (depositType.toString() === '3'){ // Unbonded let halfRewards = rewards / 2n; let nodeCommissionFee = halfRewards * nodeFee / '1'.ether; userAmount = userAmount + rewards - nodeCommissionFee; } else if (depositType.toString() === '2' || depositType.toString() === '1'){ // Half or full let halfRewards = rewards.divn(2); let nodeCommissionFee = halfRewards * nodeFee / '1'.ether; userAmount = userAmount + halfRewards - nodeCommissionFee; } else if (depositType.toString() === '4') { // Variable const nodeCapital = minipoolBalances1.nodeDepositBalance; let nodeRewards = rewards * nodeCapital / (userAmount + nodeCapital); nodeRewards = nodeRewards + ((rewards - nodeRewards) * nodeFee / '1'.ether); userAmount = userAmount + rewards - nodeRewards; } let nodeAmount = withdrawalBalance - userAmount; // Adjust amounts according to penalty rate if (penaltyRate > 0n) { let penaltyAmount = nodeAmount * penaltyRate / '1'.ether; if (penaltyRate > nodeAmount) { penaltyAmount = nodeAmount; } nodeAmount = nodeAmount - penaltyAmount; userAmount = userAmount + penaltyAmount; } // Check balances assertBN.equal(rethBalanceChange + depositPoolChange, userAmount, "rETH balance was not correct"); assertBN.equal(nodeBalanceChange, nodeAmount, "Node balance was not correct"); // If not sent from node operator then refund balance should be correct if (!(from.address === nodeWithdrawalAddress || from.address === nodeAddress)) { let refundBalance = await minipool.getNodeRefundBalance(); // console.log('Node refund balance after withdrawal:', web3.utils.fromWei(refundBalance)); assertBN.equal(refundBalance, minipoolBalances1.nodeRefundBalance + nodeAmount, "Node balance was not correct"); } } return { nodeBalanceChange, rethBalanceChange, depositPoolChange, } } export async function beginUserDistribute(minipool, txOptions) { await minipool.connect(txOptions.from).beginUserDistribute(txOptions); } ================================================ FILE: test/network/network-balances-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { registerNode, setNodeTrusted } from '../_helpers/node'; import { executeUpdateBalances, submitBalances } from './scenario-submit-balances'; import { RocketDAONodeTrustedProposals, RocketDAONodeTrustedSettingsProposals, RocketDAOProtocolSettingsNetwork, } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { daoNodeTrustedExecute, daoNodeTrustedMemberLeave, daoNodeTrustedPropose, daoNodeTrustedVote, } from '../dao/scenario-dao-node-trusted'; import { getDAOProposalEndTime, getDAOProposalStartTime } from '../dao/scenario-dao-proposal'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkBalances', () => { let owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, random; // Constants const proposalCooldown = 10; const proposalVoteBlocks = 10; const submitBalancesFrequency = 3600; // Setup before(async () => { await globalSnapShot(); [ owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, random, ] = await ethers.getSigners(); // Register node await registerNode({ from: node }); // Register trusted nodes await registerNode({ from: trustedNode1 }); await registerNode({ from: trustedNode2 }); await registerNode({ from: trustedNode3 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); await setNodeTrusted(trustedNode2, 'saas_2', 'node@home.com', owner); await setNodeTrusted(trustedNode3, 'saas_3', 'node@home.com', owner); // Set a small proposal cooldown await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown', proposalCooldown, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.blocks', proposalVoteBlocks, { from: owner }); // Set a small vote delay await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.delay.blocks', 4, { from: owner }); // Set a smaller submission frequency await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.balances.frequency', submitBalancesFrequency, { from: owner }); }); async function submitAll(block, slotTimestamp, totalBalance, stakingBalance, rethSupply) { await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1 }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2 }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode3 }); } async function trustedNode4JoinDao() { await registerNode({ from: trustedNode4 }); await setNodeTrusted(trustedNode4, 'saas_4', 'node@home.com', owner); } async function trustedNode4LeaveDao() { // Get contracts const rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Wait enough time to do a new proposal await helpers.mine(proposalCooldown); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [trustedNode4.address]); // Add the proposal let proposalId = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: trustedNode4, }); // Current block let timeCurrent = await helpers.time.latest(); // Now mine blocks until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalId) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalId, true, { from: trustedNode1 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode2 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode3 }); // Fast-forward to this voting period finishing timeCurrent = await helpers.time.latest(); await helpers.time.increase((await getDAOProposalEndTime(proposalId) - timeCurrent) + 2); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalId, { from: trustedNode1 }); // Member can now leave and collect any RPL bond await daoNodeTrustedMemberLeave(trustedNode4, { from: trustedNode4 }); } it(printTitle('trusted nodes', 'can submit network balances'), async () => { // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit different balances await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, '7'.ether, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, '6'.ether, { from: trustedNode2, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, '5'.ether, { from: trustedNode3, }); // Set parameters block = 2; // Submit identical balances to trigger update await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2, }); }); it(printTitle('trusted nodes', 'cannot submit network balances while balance submissions are disabled'), async () => { // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Disable submissions await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.balances.enabled', false, { from: owner }); // Attempt to submit balances await shouldRevert(submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }), 'Submitted balances while balance submissions were disabled'); }); it(printTitle('trusted nodes', 'cannot submit network balances for a future block'), async () => { // Get current block let blockCurrent = await ethers.provider.getBlockNumber(); // Set parameters let block = blockCurrent + 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Attempt to submit balances for future block await shouldRevert(submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }), 'Submitted balances for a future block'); }); it(printTitle('trusted nodes', 'cannot submit network balances for a lower block than recorded'), async () => { // Set parameters let block = 2; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit balances for block to trigger update await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2, }); // Attempt to submit balances for lower block await shouldRevert(submitBalances(block - 1, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode3, }), 'Submitted balances for a lower block'); }); it(printTitle('trusted nodes', 'can submit network balances for the same block as recorded (vote past consensus)'), async () => { // Set parameters let block = 2; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit balances for block to trigger update await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2, }); // Attempt to submit balances for current block await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode3, }); }); it(printTitle('trusted nodes', 'cannot submit network balances until 95% of submission frequency has passed'), async () => { // First submission is fine await submitAll(2, '1600000000', '10'.ether, '9'.ether, '8'.ether); // Wait only a brief period await helpers.time.increase(1); await helpers.mine(); // Submitting should now fail await shouldRevert( submitAll(3, '1600000001', '10.1'.ether, '9.1'.ether, '8.1'.ether), 'Was able to submit balances too soon', 'Not enough time has passed', ); // Wait enough time await helpers.time.increase(submitBalancesFrequency); await helpers.mine(); // Submitting should now work await submitAll(4, '1600000001', '10.1'.ether, '9.1'.ether, '8.1'.ether); }); it(printTitle('trusted nodes', 'cannot submit network balance change that exceeds 2%'), async () => { // First submission is fine await submitAll(2, '1600000000', '10'.ether, '9'.ether, '10'.ether); // Wait enough time await helpers.time.increase(submitBalancesFrequency); await helpers.mine(); // Submitting an increase of 2.1% should revert await shouldRevert( submitAll(3, '1600000001', '10.21'.ether, '9.1'.ether, '10'.ether), "Submitted change with greater than 2% delta", "Change exceeds maximum" ); // Submitting an decrease of 2.1% should revert await shouldRevert( submitAll(3, '1600000001', '7.9'.ether, '6.1'.ether, '10'.ether), "Submitted change with greater than 2% delta", "Change exceeds maximum" ); // Change of only 2% should work await submitAll(3, '1600000001', '10.2'.ether, '9'.ether, '10'.ether); }); it(printTitle('trusted nodes', 'cannot submit the same network balances twice'), async () => { // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit balances for block await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); // Attempt to submit balances for block again await shouldRevert(submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }), 'Submitted the same network balances twice'); }); it(printTitle('regular nodes', 'cannot submit network balances'), async () => { // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Attempt to submit balances await shouldRevert(submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: node, }), 'Regular node submitted network balances'); }); it(printTitle('random', 'can execute balances update when consensus is reached after member count changes'), async () => { // Setup await trustedNode4JoinDao(); // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit same parameters from 2 nodes (not enough for 4 member consensus but enough for 3) await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2, }); // trustedNode4 leaves the DAO await trustedNode4LeaveDao(); // There is now consensus with the remaining 3 trusted nodes about the balances, try to execute the update await executeUpdateBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: random, }); }); it(printTitle('random', 'cannot execute balances update without consensus'), async () => { // Setup await trustedNode4JoinDao(); // Set parameters let block = 1; let slotTimestamp = '1600000000'; let totalBalance = '10'.ether; let stakingBalance = '9'.ether; let rethSupply = '8'.ether; // Submit same price from 2 nodes (not enough for 4 member consensus) await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode1, }); await submitBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: trustedNode2, }); // There is no consensus so execute should fail await shouldRevert(executeUpdateBalances(block, slotTimestamp, totalBalance, stakingBalance, rethSupply, { from: random, }), 'Random account could execute update balances without consensus'); }); }); } ================================================ FILE: test/network/network-fees-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { getNodeFeeByDemand } from '../_helpers/network'; import { RocketDAOProtocolSettingsNetwork } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkFees', () => { let owner; // Constants const minNodeFee = '0.10'.ether; const targetNodeFee = '0.15'.ether; const maxNodeFee = '0.20'.ether; const demandRange = '1'.ether; // Setup before(async () => { await globalSnapShot(); [ owner, ] = await ethers.getSigners(); // Set network settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', minNodeFee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', targetNodeFee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', maxNodeFee, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.demand.range', demandRange, { from: owner }); }); it(printTitle('network node fee', 'has correct value based on node demand'), async () => { // Set expected fees for node demand values let values = [ { demand: '-1.25'.ether, expectedFee: '0.1'.ether }, { demand: '-1.00'.ether, expectedFee: '0.1'.ether }, { demand: '-0.75'.ether, expectedFee: '0.12890625'.ether }, { demand: '-0.50'.ether, expectedFee: '0.14375'.ether }, { demand: '-0.25'.ether, expectedFee: '0.14921875'.ether }, { demand: '0.00'.ether, expectedFee: '0.15'.ether }, { demand: '0.25'.ether, expectedFee: '0.15078125'.ether }, { demand: '0.50'.ether, expectedFee: '0.15625'.ether }, { demand: '0.75'.ether, expectedFee: '0.17109375'.ether }, { demand: '1.00'.ether, expectedFee: '0.2'.ether }, { demand: '1.25'.ether, expectedFee: '0.2'.ether }, ]; // Check fees for (let vi = 0; vi < values.length; ++vi) { let v = values[vi]; let nodeFee = await getNodeFeeByDemand(v.demand); assertBN.equal(nodeFee, v.expectedFee, 'Node fee does not match expected fee for node demand value'); } }); }); } ================================================ FILE: test/network/network-prices-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { registerNode, setNodeTrusted } from '../_helpers/node'; import { executeUpdatePrices, submitPrices } from './scenario-submit-prices'; import { RocketDAONodeTrustedProposals, RocketDAONodeTrustedSettingsProposals, RocketDAOProtocolSettingsNetwork, RocketNetworkPrices, } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { daoNodeTrustedExecute, daoNodeTrustedMemberLeave, daoNodeTrustedPropose, daoNodeTrustedVote, } from '../dao/scenario-dao-node-trusted'; import { getDAOProposalEndTime, getDAOProposalStartTime } from '../dao/scenario-dao-proposal'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkPrices', () => { let owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, random; // Constants const proposalCooldown = 60 * 60; const proposalVoteTime = 60 * 60; // Setup before(async () => { await globalSnapShot(); [ owner, node, trustedNode1, trustedNode2, trustedNode3, trustedNode4, random, ] = await ethers.getSigners(); // Register node await registerNode({ from: node }); // Register trusted nodes await registerNode({ from: trustedNode1 }); await registerNode({ from: trustedNode2 }); await registerNode({ from: trustedNode3 }); await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); await setNodeTrusted(trustedNode2, 'saas_2', 'node@home.com', owner); await setNodeTrusted(trustedNode3, 'saas_3', 'node@home.com', owner); // Set a small proposal cooldown await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.cooldown.time', proposalCooldown, { from: owner }); await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.time', proposalVoteTime, { from: owner }); // Set a small vote delay await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsProposals, 'proposal.vote.delay.blocks', 4, { from: owner }); }); async function trustedNode4JoinDao() { await registerNode({ from: trustedNode4 }); await setNodeTrusted(trustedNode4, 'saas_4', 'node@home.com', owner); } async function trustedNode4LeaveDao() { // Get contracts let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); // Wait enough time to do a new proposal await helpers.time.increase(proposalCooldown); // Encode the calldata for the proposal let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalLeave', [trustedNode4.address]); // Add the proposal let proposalId = await daoNodeTrustedPropose('hey guys, can I please leave the DAO?', proposalCalldata, { from: trustedNode4, }); // Current block let timeCurrent = await helpers.time.latest(); // Now mine blocks until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalId) - timeCurrent) + 2); // Now lets vote await daoNodeTrustedVote(proposalId, true, { from: trustedNode1 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode2 }); await daoNodeTrustedVote(proposalId, true, { from: trustedNode3 }); // Fast forward to this voting period finishing timeCurrent = await helpers.time.latest(); await helpers.time.increase((await getDAOProposalEndTime(proposalId) - timeCurrent) + 2); // Proposal should be successful, lets execute it await daoNodeTrustedExecute(proposalId, { from: trustedNode1 }); // Member can now leave and collect any RPL bond await daoNodeTrustedMemberLeave(trustedNode4, { from: trustedNode4 }); } it(printTitle('trusted nodes', 'can submit network prices'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit different prices await submitPrices(block, slotTimestamp, '0.03'.ether, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, '0.04'.ether, { from: trustedNode2, }); await submitPrices(block, slotTimestamp, '0.05'.ether, { from: trustedNode3, }); // Set parameters block = await ethers.provider.getBlockNumber(); // Submit identical prices to trigger update await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode2, }); }); it(printTitle('trusted nodes', 'cannot submit network prices while price submissions are disabled'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Disable submissions await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.prices.enabled', false, { from: owner }); // Attempt to submit prices await shouldRevert(submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }), 'Submitted prices while price submissions were disabled'); }); it(printTitle('trusted nodes', 'cannot submit network prices for a future block'), async () => { // Get current block let blockCurrent = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; // Set parameters let block = blockCurrent + 1; let rplPrice = '0.02'.ether; // Attempt to submit prices for future block await shouldRevert(submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }), 'Submitted prices for a future block'); }); it(printTitle('trusted nodes', 'cannot submit network prices for a lower block than recorded'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit prices for block to trigger update await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode2, }); // Attempt to submit prices for lower block await shouldRevert(submitPrices(block - 1, slotTimestamp, rplPrice, { from: trustedNode3, }), 'Submitted prices for a lower block'); }); it(printTitle('trusted nodes', 'can submit network prices for the current recorded block (vote past consensus)'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit prices for block to trigger update await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode2, }); // Attempt to submit prices for current block await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode3, }); }); it(printTitle('trusted nodes', 'cannot submit the same network prices twice'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit prices for block await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); // Attempt to submit prices for block again await shouldRevert(submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }), 'Submitted the same network prices twice'); }); it(printTitle('regular nodes', 'cannot submit network prices'), async () => { // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Attempt to submit prices await shouldRevert(submitPrices(block, slotTimestamp, rplPrice, { from: node, }), 'Regular node submitted network prices'); }); it(printTitle('random', 'can execute price update when consensus is reached after member count changes'), async () => { // Setup await trustedNode4JoinDao(); // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit same price from 2 nodes (not enough for 4 member consensus but enough for 3) await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode2, }); // trustedNode4 leaves the DAO await trustedNode4LeaveDao(); // There is now consensus with the remaining 3 trusted nodes about the price, try to execute the update await executeUpdatePrices(block, slotTimestamp, rplPrice, { from: random, }); }); it(printTitle('random', 'cannot execute price update without consensus'), async () => { // Setup await trustedNode4JoinDao(); // Set parameters let block = await ethers.provider.getBlockNumber(); let slotTimestamp = '1600000000'; let rplPrice = '0.02'.ether; // Submit same price from 2 nodes (not enough for 4 member consensus) await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode1, }); await submitPrices(block, slotTimestamp, rplPrice, { from: trustedNode2, }); // There is no consensus so execute should fail await shouldRevert(executeUpdatePrices(block, slotTimestamp, rplPrice, { from: random, }), 'Random account could execute update prices without consensus'); }); }); } ================================================ FILE: test/network/network-revenues-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { RocketDAOProtocolSettingsNetwork, RocketNetworkRevenues } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkRevenues', () => { let owner; // Setup before(async () => { await globalSnapShot(); [ owner, ] = await ethers.getSigners(); }); it(printTitle('revenue split', 'calculates correct time weighted average node share'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); // Initial value should be 5% const shareBefore = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(shareBefore, '0.05'.ether); // Set value to 10% and check await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.node.commission.share", '0.10'.ether, { from: owner }); const shareAfter = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(shareAfter, '0.10'.ether); // Mine 2 blocks await helpers.time.increase(2); // Get calculated shares const block = await ethers.provider.getBlock(); const calculatedShare = await rocketNetworkRevenues.calculateSplit(block.timestamp - 3); // 1 day of 5% and 2 days of 10% should average out to 8.33% (math is done in 3 decimal fixed point) assertBN.equal(calculatedShare[0], '0.08333'.ether); }); it(printTitle('revenue split', 'calculates correct time weighted average pdao share'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); // Initial value should be 0% const shareBefore = await rocketNetworkRevenues.getCurrentProtocolDAOShare(); assertBN.equal(shareBefore, '0'.ether); // Mine 10 blocks await helpers.time.increase(10); // Set value to 1% and check await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.pdao.share", '0.01'.ether, { from: owner }); const shareAfter = await rocketNetworkRevenues.getCurrentProtocolDAOShare(); assertBN.equal(shareAfter, '0.01'.ether); // Mine 2 blocks await helpers.time.increase(20); // Get calculated shares const block = await ethers.provider.getBlock(); const calculatedShare = await rocketNetworkRevenues.calculateSplit(block.timestamp - 30); // 10 days at 0% and 20 days at 1% should average out to 0.666% (math is done in 3 decimal fixed point) assertBN.equal(calculatedShare[2], '0.00666'.ether); }); it(printTitle('revenue split', 'calculates correct shares when using the adder'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); // Increment the adder by 0.5% const adder = '0.005'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.node.commission.share.security.council.adder", adder, { from: owner }); // Check node share const effectiveNodeShare = await rocketDAOProtocolSettingsNetwork.getEffectiveNodeShare(); assertBN.equal(effectiveNodeShare, '0.05'.ether + adder); const nodeShare = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(nodeShare, '0.05'.ether + adder); // Check voter share const effectiveVoterShare = await rocketDAOProtocolSettingsNetwork.getEffectiveVoterShare(); assertBN.equal(effectiveVoterShare, '0.09'.ether - adder); const voterShare = await rocketNetworkRevenues.getCurrentVoterShare(); assertBN.equal(voterShare, '0.09'.ether - adder); }); it(printTitle('revenue split', 'calculates correct shares when using the adder and pdao share'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); const rocketDAOProtocolSettingsNetwork = await RocketDAOProtocolSettingsNetwork.deployed(); // Set the protocol dao share to 1% const pdaoShare = '0.01'.ether await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.pdao.share", pdaoShare, { from: owner }); // Increment the adder by 0.5% const adder = '0.005'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.node.commission.share.security.council.adder", adder, { from: owner }); // Check node share const effectiveNodeShare = await rocketDAOProtocolSettingsNetwork.getEffectiveNodeShare(); assertBN.equal(effectiveNodeShare, '0.05'.ether + adder); const nodeShare = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(nodeShare, '0.05'.ether + adder); // Check voter share const effectiveVoterShare = await rocketDAOProtocolSettingsNetwork.getEffectiveVoterShare(); assertBN.equal(effectiveVoterShare, '0.09'.ether - adder); const voterShare = await rocketNetworkRevenues.getCurrentVoterShare(); assertBN.equal(voterShare, '0.09'.ether - adder); // Check pdao share const networkPdaoShare = await rocketDAOProtocolSettingsNetwork.getProtocolDAOShare(); assertBN.equal(networkPdaoShare, '0.01'.ether); }); it(printTitle('revenue split', 'calculates correct time weighted average node share after adder is used'), async () => { const rocketNetworkRevenues = await RocketNetworkRevenues.deployed(); // Initial value should be 5% const shareBefore = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(shareBefore, '0.05'.ether); // Set value to 10% and check const adder = '0.005'.ether; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, "network.node.commission.share.security.council.adder", adder, { from: owner }); const shareAfter = await rocketNetworkRevenues.getCurrentNodeShare(); assertBN.equal(shareAfter, '0.05'.ether + adder); // Mine 2 blocks await helpers.time.increase(2); // Get calculated shares const block = await ethers.provider.getBlock(); const calculatedShare = await rocketNetworkRevenues.calculateSplit(block.timestamp - 3); // 1 day of 5% and 2 days of 5.5% should average out to 5.33% (math is done in 3 decimal fixed point) assertBN.equal(calculatedShare[0], '0.05333'.ether); }); }); } ================================================ FILE: test/network/network-snapshots-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { RocketNetworkSnapshots, RocketNetworkSnapshotsTime, RocketStorage, SnapshotTest, SnapshotTimeTest, } from '../_utils/artifacts'; import { setDaoNodeTrustedBootstrapUpgrade } from '../dao/scenario-dao-node-trusted-bootstrap'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkSnapshots', () => { let owner; let snapshotTest; let snapshotTimeTest; let networkSnapshots; let networkSnapshotsTime; // Setup before(async () => { await globalSnapShot(); [ owner, ] = await ethers.getSigners(); // Add snapshot helper contracts const rocketStorage = await RocketStorage.deployed(); snapshotTest = await SnapshotTest.new(rocketStorage.target, { from: owner }); await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketSnapshotTest', SnapshotTest.abi, snapshotTest.target, { from: owner, }); snapshotTimeTest = await SnapshotTimeTest.new(rocketStorage.target, { from: owner }); await setDaoNodeTrustedBootstrapUpgrade('addContract', 'rocketSnapshotTimeTest', SnapshotTimeTest.abi, snapshotTimeTest.target, { from: owner, }); // Get contracts networkSnapshots = await RocketNetworkSnapshots.deployed(); networkSnapshotsTime = await RocketNetworkSnapshotsTime.deployed(); }); it(printTitle('contract', 'can insert values into snapshot'), async () => { const blockNumber = await ethers.provider.getBlockNumber(); await snapshotTest.push('test', '50'.BN); // block + 1 await snapshotTest.push('test', '150'.BN); // block + 2 await helpers.mine(2); await snapshotTest.push('test', '250'.BN); // block + 5 assertBN.equal(await snapshotTest.lookup('test', blockNumber + 1), '50'.BN); assertBN.equal(await snapshotTest.lookup('test', blockNumber + 2), '150'.BN); assertBN.equal(await snapshotTest.lookup('test', blockNumber + 3), '150'.BN); assertBN.equal(await snapshotTest.lookup('test', blockNumber + 4), '150'.BN); assertBN.equal(await snapshotTest.lookup('test', blockNumber + 5), '250'.BN); assertBN.equal(await snapshotTest.lookupRecent('test', blockNumber + 1, 10), '50'.BN); assertBN.equal(await snapshotTest.lookupRecent('test', blockNumber + 2, 10), '150'.BN); assertBN.equal(await snapshotTest.lookupRecent('test', blockNumber + 3, 10), '150'.BN); assertBN.equal(await snapshotTest.lookupRecent('test', blockNumber + 4, 10), '150'.BN); assertBN.equal(await snapshotTest.lookupRecent('test', blockNumber + 5, 10), '250'.BN); }); it(printTitle('contract', 'can insert values into time-based snapshot'), async () => { const startTime = await helpers.time.latest() await helpers.time.setNextBlockTimestamp(startTime + 12) await snapshotTimeTest.push('test', '50'.BN); // block + 12 s await helpers.time.setNextBlockTimestamp(startTime + 24) await snapshotTimeTest.push('test', '150'.BN); // block + 24 s await helpers.time.setNextBlockTimestamp(startTime + 60) await snapshotTimeTest.push('test', '250'.BN); // block + 60 s assertBN.equal(await snapshotTimeTest.lookup('test', startTime + 12), '50'.BN); assertBN.equal(await snapshotTimeTest.lookup('test', startTime + 24), '150'.BN); assertBN.equal(await snapshotTimeTest.lookup('test', startTime + 36), '150'.BN); assertBN.equal(await snapshotTimeTest.lookup('test', startTime + 48), '150'.BN); assertBN.equal(await snapshotTimeTest.lookup('test', startTime + 60), '250'.BN); assertBN.equal(await snapshotTimeTest.lookupRecent('test', startTime + 12, 10), '50'.BN); assertBN.equal(await snapshotTimeTest.lookupRecent('test', startTime + 24, 10), '150'.BN); assertBN.equal(await snapshotTimeTest.lookupRecent('test', startTime + 36, 10), '150'.BN); assertBN.equal(await snapshotTimeTest.lookupRecent('test', startTime + 48, 10), '150'.BN); assertBN.equal(await snapshotTimeTest.lookupRecent('test', startTime + 60, 10), '250'.BN); }); }); } ================================================ FILE: test/network/network-voting-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { RocketNetworkSnapshots, RocketNetworkVoting } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { nodeStakeRPL, registerNode } from '../_helpers/node'; import { createMinipool, getMinipoolMaximumRPLStake } from '../_helpers/minipool'; import { mintRPL } from '../_helpers/tokens'; import { userDeposit } from '../_helpers/deposit'; import { globalSnapShot } from '../_utils/snapshotting'; import { BigSqrt } from '../_helpers/bigmath'; import { nodeDeposit, nodeDepositMulti } from '../_helpers/megapool'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNetworkVoting', () => { let owner, node, random; let networkSnapshots; let networkVoting; // Setup before(async () => { await globalSnapShot(); [ owner, node, random, ] = await ethers.getSigners(); // Get contracts networkSnapshots = await RocketNetworkSnapshots.deployed(); networkVoting = await RocketNetworkVoting.deployed(); // Register node & set withdrawal address await registerNode({ from: node }); // Stake RPL for voting power let rplStake = '1200'.ether; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, { from: node }); // Add some ETH into the DP await userDeposit({ from: random, value: '320'.ether }); }); it(printTitle('Voting Power', 'Should correctly snapshot values'), async () => { // Create a minipool to set the active count to non-zero await nodeDeposit(node, '4'.ether, false); const blockBefore = (await ethers.provider.getBlockNumber()); await nodeDeposit(node, '4'.ether, false); const blockAfter = (await ethers.provider.getBlockNumber()); const votingPowerBefore = await networkVoting.getVotingPower(node, blockBefore); const votingPowerAfter = await networkVoting.getVotingPower(node, blockAfter); assertBN.equal(votingPowerBefore, BigSqrt('600'.ether * '1'.ether)); assertBN.equal(votingPowerAfter, BigSqrt('1200'.ether * '1'.ether)); }); }); } ================================================ FILE: test/network/scenario-submit-balances.js ================================================ import { RocketDAONodeTrusted, RocketDAOProtocolSettingsNetwork, RocketNetworkBalances, RocketStorage, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; import { getNetworkSetting } from '../_helpers/settings'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit network balances export async function submitBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions) { // Load contracts const [ rocketDAONodeTrusted, rocketNetworkBalances, rocketStorage, rocketDAOProtocolSettingsNetwork ] = await Promise.all([ RocketDAONodeTrusted.deployed(), RocketNetworkBalances.deployed(), RocketStorage.deployed(), RocketDAOProtocolSettingsNetwork.deployed(), ]); // Get parameters let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); const maxRethDelta = await rocketDAOProtocolSettingsNetwork.getMaxRethDelta(); // Get submission keys let nodeSubmissionKey = ethers.solidityPackedKeccak256( ['string', 'address', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256'], ['network.balances.submitted.node', txOptions.from.address, block, slotTimestamp, totalEth, stakingEth, rethSupply] ); let submissionCountKey = ethers.solidityPackedKeccak256( ['string', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256'], ['network.balances.submitted.count', block, slotTimestamp, totalEth, stakingEth, rethSupply] ); // Get submission details function getSubmissionDetails() { return Promise.all([ rocketStorage.getBool(nodeSubmissionKey), rocketStorage.getUint(submissionCountKey), ]).then( ([nodeSubmitted, count]) => ({ nodeSubmitted, count }), ); } // Get balances function getBalances() { return Promise.all([ rocketNetworkBalances.getBalancesBlock(), rocketNetworkBalances.getTotalETHBalance(), rocketNetworkBalances.getStakingETHBalance(), rocketNetworkBalances.getTotalRETHSupply(), ]).then( ([block, totalEth, stakingEth, rethSupply]) => ({ block, totalEth, stakingEth, rethSupply }), ); } // Get initial submission details let [submission1, balances1] = await Promise.all([ getSubmissionDetails(), getBalances(), ]); // Submit balances await rocketNetworkBalances.connect(txOptions.from).submitBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions); // Get updated submission details & balances let [submission2, balances2] = await Promise.all([ getSubmissionDetails(), getBalances(), ]); // Check if balances should be updated let expectUpdatedBalances = ((submission2.count * 2n) > trustedNodeCount); // Check submission details assert.equal(submission1.nodeSubmitted, false, 'Incorrect initial node submitted status'); assert.equal(submission2.nodeSubmitted, true, 'Incorrect updated node submitted status'); assertBN.equal(submission2.count, submission1.count + 1n, 'Incorrect updated submission count'); // Skip balance checks because submission was already executed if (balances1.block === BigInt(block)) return; // Check balances if (expectUpdatedBalances) { let expectedTotalEth = totalEth; if (balances1.totalEth > 0n) { const maxChange = balances1.totalEth * maxRethDelta / '1'.ether; const change = totalEth - balances1.totalEth; if (change > maxChange) { expectedTotalEth = balances1.totalEth + maxChange; } if (change < -maxChange) { expectedTotalEth = balances1.totalEth - maxChange; } } assertBN.equal(balances2.block, block, 'Incorrect updated network balances block'); assertBN.equal(balances2.totalEth, expectedTotalEth, 'Incorrect updated network total ETH balance'); assertBN.equal(balances2.stakingEth, stakingEth, 'Incorrect updated network staking ETH balance'); assertBN.equal(balances2.rethSupply, rethSupply, 'Incorrect updated network total rETH supply'); } else { assertBN.equal(balances2.block, balances1.block, 'Incorrectly updated network balances block'); assertBN.equal(balances2.totalEth, balances1.totalEth, 'Incorrectly updated network total ETH balance'); assertBN.equal(balances2.stakingEth, balances1.stakingEth, 'Incorrectly updated network staking ETH balance'); assertBN.equal(balances2.rethSupply, balances1.rethSupply, 'Incorrectly updated network total rETH supply'); } } // Execute update network balances export async function executeUpdateBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions) { // Load contracts const rocketNetworkBalances = await RocketNetworkBalances.deployed(); // Get balances function getBalances() { return Promise.all([ rocketNetworkBalances.getBalancesBlock(), rocketNetworkBalances.getTotalETHBalance(), rocketNetworkBalances.getStakingETHBalance(), rocketNetworkBalances.getTotalRETHSupply(), ]).then( ([block, totalEth, stakingEth, rethSupply]) => ({ block, totalEth, stakingEth, rethSupply }), ); } // Submit balances await rocketNetworkBalances.connect(txOptions.from).executeUpdateBalances(block, slotTimestamp, totalEth, stakingEth, rethSupply, txOptions); // Get updated balances let balances = await getBalances(); // Check balances assertBN.equal(balances.block, block, 'Incorrect updated network balances block'); assertBN.equal(balances.totalEth, totalEth, 'Incorrect updated network total ETH balance'); assertBN.equal(balances.stakingEth, stakingEth, 'Incorrect updated network staking ETH balance'); assertBN.equal(balances.rethSupply, rethSupply, 'Incorrect updated network total rETH supply'); } ================================================ FILE: test/network/scenario-submit-penalties.js ================================================ import { RocketDAONodeTrusted, RocketDAOProtocolSettingsNetwork, RocketMinipoolPenalty, RocketNetworkPenalties, RocketStorage, } from '../_utils/artifacts'; import { shouldRevert } from '../_utils/testing'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit network penalties export async function submitPenalty(minipoolAddress, block, txOptions) { // Load contracts const [ rocketDAONodeTrusted, rocketNetworkPenalties, rocketMinipoolPenalty, rocketStorage, rocketDAOProtocolSettingsNetwork, ] = await Promise.all([ RocketDAONodeTrusted.deployed(), RocketNetworkPenalties.deployed(), RocketMinipoolPenalty.deployed(), RocketStorage.deployed(), RocketDAOProtocolSettingsNetwork.deployed(), ]); // Get parameters let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); // Get submission keys let penaltyKey = ethers.solidityPackedKeccak256(['string', 'address'], ['network.penalties.penalty', minipoolAddress]); let nodeSubmissionKey = ethers.solidityPackedKeccak256(['string', 'address', 'address', 'uint256'], ['minipool.penalty.submission', txOptions.from.address, minipoolAddress, block]); let submissionCountKey = ethers.solidityPackedKeccak256(['string', 'address', 'uint256'], ['minipool.penalty.submission', minipoolAddress, block]); let executionKey = ethers.solidityPackedKeccak256(['string', 'address', 'uint256'], ['minipool.penalty.submission.applied', minipoolAddress, block]); let maxPenaltyRate = await rocketMinipoolPenalty.getMaxPenaltyRate(); let penaltyThreshold = await rocketDAOProtocolSettingsNetwork.getNodePenaltyThreshold(); // Get submission details function getSubmissionDetails() { return Promise.all([ rocketStorage.getBool(nodeSubmissionKey), rocketStorage.getUint(submissionCountKey), rocketStorage.getBool(executionKey), ]).then( ([nodeSubmitted, count, executed]) => ({ nodeSubmitted, count, executed }), ); } function getPenalty() { return Promise.all([ rocketMinipoolPenalty.getPenaltyRate(minipoolAddress), rocketStorage.getUint(penaltyKey), ]).then( ([penaltyRate, penaltyCount]) => ({ penaltyRate, penaltyCount }), ); } // Get initial submission details let [submission1, penalty1] = await Promise.all([ getSubmissionDetails(), getPenalty(), ]); // Submit penalties if (submission1.executed) { await shouldRevert(rocketNetworkPenalties.connect(txOptions.from).submitPenalty(minipoolAddress, block, txOptions), 'Did not revert on already executed penalty', 'Penalty already applied'); } else { await rocketNetworkPenalties.connect(txOptions.from).submitPenalty(minipoolAddress, block, txOptions); } // Get updated submission details & penalties let [submission2, penalty2] = await Promise.all([ getSubmissionDetails(), getPenalty(), ]); // Check if penalties should be updated let expectedUpdatedPenalty = ('1'.ether * submission2.count / trustedNodeCount) >= penaltyThreshold; // Check submission details assert.equal(submission1.nodeSubmitted, false, 'Incorrect initial node submitted status'); if (!submission1.executed) { assert.equal(submission2.nodeSubmitted, true, 'Incorrect updated node submitted status'); assertBN.equal(submission2.count, submission1.count + 1n, 'Incorrect updated submission count'); } // Check penalty if (!submission1.executed && expectedUpdatedPenalty) { assert.equal(submission2.executed, true, 'Penalty not executed'); assertBN.equal(penalty2.penaltyCount, penalty1.penaltyCount + 1n, 'Penalty count not updated'); // Unless we hit max penalty, expect to see an increase in the penalty rate if (penalty1.penaltyRate < maxPenaltyRate && penalty2.penaltyCount >= 3n) { assertBN.isAbove(penalty2.penaltyRate, penalty1.penaltyRate, 'Penalty rate did not increase'); } } else if (!expectedUpdatedPenalty) { assert.equal(submission2.executed, false, 'Penalty executed'); } } ================================================ FILE: test/network/scenario-submit-prices.js ================================================ import { RocketDAONodeTrusted, RocketNetworkPrices, RocketStorage } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit network prices export async function submitPrices(block, slotTimestamp, rplPrice, txOptions) { // Load contracts const [ rocketDAONodeTrusted, rocketNetworkPrices, rocketStorage, ] = await Promise.all([ RocketDAONodeTrusted.deployed(), RocketNetworkPrices.deployed(), RocketStorage.deployed(), ]); // Get parameters let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); // Get submission keys let nodeSubmissionKey = ethers.solidityPackedKeccak256( ['string', 'address', 'uint256', 'uint256', 'uint256'], ['network.prices.submitted.node.key', txOptions.from.address, block, slotTimestamp, rplPrice], ); let submissionCountKey = ethers.solidityPackedKeccak256( ['string', 'uint256', 'uint256', 'uint256'], ['network.prices.submitted.count', block, slotTimestamp, rplPrice], ); // Get submission details function getSubmissionDetails() { return Promise.all([ rocketStorage.getBool(nodeSubmissionKey), rocketStorage.getUint(submissionCountKey), ]).then( ([nodeSubmitted, count]) => ({ nodeSubmitted, count }), ); } // Get prices function getPrices() { return Promise.all([ rocketNetworkPrices.getPricesBlock(), rocketNetworkPrices.getRPLPrice(), ]).then( ([block, rplPrice]) => ({ block, rplPrice }), ); } // Get initial submission details let submission1 = await getSubmissionDetails(); // Submit prices await rocketNetworkPrices.connect(txOptions.from).submitPrices(block, slotTimestamp, rplPrice, txOptions); // Get updated submission details & prices let [submission2, prices] = await Promise.all([ getSubmissionDetails(), getPrices(), ]); // Check if prices should be updated let expectUpdatedPrices = (submission2.count * 2n) > trustedNodeCount; // Check submission details assert.equal(submission1.nodeSubmitted, false, 'Incorrect initial node submitted status'); assert.equal(submission2.nodeSubmitted, true, 'Incorrect updated node submitted status'); assertBN.equal(submission2.count, submission1.count + 1n, 'Incorrect updated submission count'); // Check prices if (expectUpdatedPrices) { assertBN.equal(prices.block, block, 'Incorrect updated network prices block'); assertBN.equal(prices.rplPrice, rplPrice, 'Incorrect updated network RPL price'); } else { assertBN.notEqual(prices.block, block, 'Incorrectly updated network prices block'); assertBN.notEqual(prices.rplPrice, rplPrice, 'Incorrectly updated network RPL price'); } } // Execute price update export async function executeUpdatePrices(block, slotTimestamp, rplPrice, txOptions) { // Load contracts const rocketNetworkPrices = await RocketNetworkPrices.deployed(); // Get prices function getPrices() { return Promise.all([ rocketNetworkPrices.getPricesBlock(), rocketNetworkPrices.getRPLPrice(), ]).then( ([block, rplPrice]) => ({ block, rplPrice }), ); } // Submit prices await rocketNetworkPrices.connect(txOptions.from).executeUpdatePrices(block, slotTimestamp, rplPrice, txOptions); // Get updated submission details & prices let prices = await getPrices(); // Check the prices assertBN.equal(prices.block, block, 'Incorrect updated network prices block'); assertBN.equal(prices.rplPrice, rplPrice, 'Incorrect updated network RPL price'); } ================================================ FILE: test/node/node-distributor-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { RevertOnTransfer, RocketNodeDistributorFactory, RocketNodeManager } from '../_utils/artifacts'; import { registerNode, setNodeTrusted, setNodeWithdrawalAddress } from '../_helpers/node'; import { distributeRewards } from './scenario-distribute-rewards'; import { globalSnapShot } from '../_utils/snapshotting'; import { assertBN } from '../_helpers/bn'; import { shouldRevert } from '../_utils/testing'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNodeDistributor', () => { let owner, node1, node2, node1WithdrawalAddress, trustedNode, random; let distributorAddress; let rplStake; // Setup before(async () => { await globalSnapShot(); [ owner, node1, node2, node1WithdrawalAddress, trustedNode, random, ] = await ethers.getSigners(); // Get contracts const rocketNodeDistributorFactory = await RocketNodeDistributorFactory.deployed(); // Register node await registerNode({ from: node1 }); distributorAddress = await rocketNodeDistributorFactory.getProxyAddress(node1); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); // The protocol no longer automatically deploys a fee distributor as it is not used for megapools const rocketNodeManager = await RocketNodeManager.deployed(); await rocketNodeManager.connect(node1).initialiseFeeDistributor(); }); it(printTitle('random', 'can distribute rewards'), async () => { // Send some funds to the distributor await owner.sendTransaction({ to: distributorAddress, value: '1'.ether, }); // Distributing rewards should not fail await distributeRewards(node1, { from: random }); // Check unclaimed rewards was increased const rocketNodeManager = await RocketNodeManager.deployed(); const unclaimedRewards = await rocketNodeManager.getUnclaimedRewards(node1.address); // The default capital ratio is 1:1 with 0% fee, therefore NO should have 0.5 ETH unclaimed assertBN.equal(unclaimedRewards, '0.5'.ether); }); it(printTitle('node', 'can not manually add unclaimed rewards'), async () => { const rocketNodeManager = await RocketNodeManager.deployed(); await shouldRevert( rocketNodeManager.connect(node1).addUnclaimedRewards(node1.address, { value: '1'.ether }), 'Was able to call addUnclaimedRewards', 'Only distributor can add unclaimed rewards' ); }); it(printTitle('node', 'can distribute rewards directly to withdrawal address'), async () => { // Set node withdrawal address to reverting helper await setNodeWithdrawalAddress(node1.address, node1WithdrawalAddress.address, { from: node1 }); // Send some funds to the distributor await owner.sendTransaction({ to: distributorAddress, value: '1'.ether, }); // Get withdrawal address balance before const withdrawalAddressBalanceBefore = await ethers.provider.getBalance(node1WithdrawalAddress.address) // Distributing rewards should not fail await distributeRewards(node1, { from: node1 }); // Check unclaimed rewards was increased const rocketNodeManager = await RocketNodeManager.deployed(); const unclaimedRewards = await rocketNodeManager.getUnclaimedRewards(node1.address); // Should be no unclaimed rewards assertBN.equal(unclaimedRewards, '0'.ether); // Withdrawal address should now have the 0.5 ETH const withdrawalAddressBalanceAfter = await ethers.provider.getBalance(node1WithdrawalAddress.address) assertBN.equal(withdrawalAddressBalanceAfter - withdrawalAddressBalanceBefore, '0.5'.ether); }); describe('With unclaimed rewards from reverting withdrawal contract', () => { let revertOnTransfer; before(async () => { // Enable reverting on transfer helper revertOnTransfer = await RevertOnTransfer.deployed(); await revertOnTransfer.setEnabled(true); // Set node withdrawal address to reverting helper await setNodeWithdrawalAddress(node1.address, revertOnTransfer.target, { from: node1 }); // Send some funds to the distributor await owner.sendTransaction({ to: distributorAddress, value: '1'.ether, }); // Distributing rewards should not fail await distributeRewards(node1, { from: owner }); // Check unclaimed rewards was increased const rocketNodeManager = await RocketNodeManager.deployed(); const unclaimedRewards = await rocketNodeManager.getUnclaimedRewards(node1.address); // The default capital ratio is 1:1 with 0% fee, therefore NO should have 0.5 ETH unclaimed assertBN.equal(unclaimedRewards, '0.5'.ether); // Disable revert await revertOnTransfer.setEnabled(false); }) it(printTitle('node operator', 'can claim unclaimed rewards'), async () => { // Try to claim const rocketNodeManager = await RocketNodeManager.deployed(); await rocketNodeManager.connect(node1).claimUnclaimedRewards(node1.address); const unclaimedRewards = await rocketNodeManager.getUnclaimedRewards(node1.address); // Should be no unclaimed rewards now assertBN.equal(unclaimedRewards, '0'.ether); // Withdrawal address should now have the 0.5 ETH const withdrawalAddressBalance = await ethers.provider.getBalance(revertOnTransfer.target) assertBN.equal(withdrawalAddressBalance, '0.5'.ether); }); it(printTitle('random', 'can not claim unclaimed rewards'), async () => { // Try to claim const rocketNodeManager = await RocketNodeManager.deployed(); await shouldRevert( rocketNodeManager.connect(random).claimUnclaimedRewards(node1.address), 'Was able to claim', 'Only node can claim' ); }); }) }); } ================================================ FILE: test/node/node-manager-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { registerNode } from '../_helpers/node'; import { RocketDAOProtocolSettingsNode, RocketNodeManager } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting, setRewardsClaimIntervalTime } from '../dao/scenario-dao-protocol-bootstrap'; import { register } from './scenario-register'; import { setTimezoneLocation } from './scenario-set-timezone'; import { confirmWithdrawalAddress, setWithdrawalAddress } from './scenario-set-withdrawal-address'; import { setSmoothingPoolRegistrationState } from './scenario-register-smoothing-pool'; import { globalSnapShot } from '../_utils/snapshotting'; import * as assert from 'assert'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNodeManager', () => { let owner, node, registeredNode1, registeredNode2, withdrawalAddress1, withdrawalAddress2, withdrawalAddress3, random, random2, random3; // Constants const ONE_DAY = 24 * 60 * 60; const claimIntervalTime = ONE_DAY * 28; // Setup before(async () => { await globalSnapShot(); [ owner, node, registeredNode1, registeredNode2, withdrawalAddress1, withdrawalAddress2, withdrawalAddress3, random, random2, random3, ] = await ethers.getSigners(); // Enable smoothing pool registrations await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.smoothing.pool.registration.enabled', true, { from: owner }); // Register nodes await registerNode({ from: registeredNode1 }); await registerNode({ from: registeredNode2 }); // Set the claim interval blocks await setRewardsClaimIntervalTime(claimIntervalTime, { from: owner }); }); // // Registration // it(printTitle('node operator', 'can register a node'), async () => { // Register node await register('Australia/Brisbane', { from: node, }); }); it(printTitle('node operator', 'cannot register a node while registrations are disabled'), async () => { // Disable registrations await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.registration.enabled', false, { from: owner }); // Attempt registration await shouldRevert(register('Australia/Brisbane', { from: node, }), 'Registered a node while registrations were disabled'); }); it(printTitle('node operator', 'cannot register a node with an invalid timezone location'), async () => { // Attempt to register node await shouldRevert(register('a', { from: node, }), 'Registered a node with an invalid timezone location'); }); it(printTitle('node operator', 'cannot register a node which is already registered'), async () => { // Register await register('Australia/Brisbane', { from: node }); // Attempt second registration await shouldRevert(register('Australia/Brisbane', { from: node, }), 'Registered a node which is already registered'); }); // // Withdrawal address // it(printTitle('node operator', 'can set their withdrawal address immediately'), async () => { // Set withdrawal address await setWithdrawalAddress(registeredNode1, withdrawalAddress1.address, true, { from: registeredNode1, }); // Set withdrawal address again await setWithdrawalAddress(registeredNode1, withdrawalAddress2.address, true, { from: withdrawalAddress1, }); }); it(printTitle('node operator', 'can set their withdrawal address to the same value as another node operator'), async () => { // Set withdrawal addresses await setWithdrawalAddress(registeredNode1, withdrawalAddress1.address, true, { from: registeredNode1, }); await setWithdrawalAddress(registeredNode2, withdrawalAddress1.address, true, { from: registeredNode2, }); // Set withdrawal addresses again await setWithdrawalAddress(registeredNode1, withdrawalAddress2.address, true, { from: withdrawalAddress1, }); await setWithdrawalAddress(registeredNode2, withdrawalAddress2.address, true, { from: withdrawalAddress1, }); }); it(printTitle('node operator', 'cannot set their withdrawal address to an invalid address'), async () => { // Attempt to set withdrawal address await shouldRevert(setWithdrawalAddress(registeredNode1, '0x0000000000000000000000000000000000000000', true, { from: registeredNode1, }), 'Set a withdrawal address to an invalid address'); }); it(printTitle('random address', 'cannot set a withdrawal address'), async () => { // Attempt to set withdrawal address await shouldRevert(setWithdrawalAddress(registeredNode1, withdrawalAddress1.address, true, { from: random, }), 'Random address set a withdrawal address'); }); it(printTitle('node operator', 'can set and confirm their withdrawal address'), async () => { // Set & confirm withdrawal address await setWithdrawalAddress(registeredNode1, withdrawalAddress1.address, false, { from: registeredNode1, }); await confirmWithdrawalAddress(registeredNode1, { from: withdrawalAddress1, }); // Set & confirm withdrawal address again await setWithdrawalAddress(registeredNode1, withdrawalAddress2.address, false, { from: withdrawalAddress1, }); await confirmWithdrawalAddress(registeredNode1, { from: withdrawalAddress2, }); }); it(printTitle('random address', 'cannot confirm itself as a withdrawal address'), async () => { // Attempt to confirm a withdrawal address await shouldRevert(confirmWithdrawalAddress(registeredNode1, { from: random, }), 'Random address confirmed itself as a withdrawal address'); }); // // Timezone location // it(printTitle('node operator', 'can set their timezone location'), async () => { // Set timezone location await setTimezoneLocation('Australia/Sydney', { from: registeredNode1, }); }); it(printTitle('node operator', 'cannot set their timezone location to an invalid value'), async () => { // Attempt to set timezone location await shouldRevert(setTimezoneLocation('a', { from: registeredNode1, }), 'Set a timezone location to an invalid value'); }); it(printTitle('random address', 'cannot set a timezone location'), async () => { // Attempt to set timezone location await shouldRevert(setTimezoneLocation('Australia/Brisbane', { from: random, }), 'Random address set a timezone location'); }); // // Smoothing pool // it(printTitle('node operator', 'can not register for smoothing pool if registrations are disabled'), async () => { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.smoothing.pool.registration.enabled', false, { from: owner }); await shouldRevert(setSmoothingPoolRegistrationState(true, { from: registeredNode1 }), 'Was able to register while registrations were disabled', 'Smoothing pool registrations are not active'); }); it(printTitle('node operator', 'can set their smoothing pool registration state'), async () => { await setSmoothingPoolRegistrationState(true, { from: registeredNode1 }); }); it(printTitle('node operator', 'can not set their smoothing pool registration state to the same value'), async () => { await shouldRevert(setSmoothingPoolRegistrationState(false, { from: registeredNode1 }), 'Was able to change smoothing pool registration state', 'Invalid state change'); }); it(printTitle('node operator', 'can not set their smoothing pool registration state before a reward interval has passed'), async () => { await setSmoothingPoolRegistrationState(true, { from: registeredNode1 }); await shouldRevert(setSmoothingPoolRegistrationState(false, { from: registeredNode1 }), 'Was able to change smoothing pool registration state', 'Not enough time has passed since changing state'); }); it(printTitle('node operator', 'can set their smoothing pool registration state after a reward interval has passed'), async () => { await setSmoothingPoolRegistrationState(true, { from: registeredNode1 }); await helpers.time.increase(claimIntervalTime + 1); await setSmoothingPoolRegistrationState(false, { from: registeredNode1 }); }); // // Misc // it(printTitle('random', 'can query timezone counts'), async () => { const rocketNodeManager = await RocketNodeManager.deployed(); await rocketNodeManager.connect(random2).registerNode('Australia/Sydney'); await rocketNodeManager.connect(random3).registerNode('Australia/Perth'); const timezones = await rocketNodeManager.getNodeCountPerTimezone(0, 0); const expects = { 'Australia/Brisbane': 2, 'Australia/Sydney': 1, 'Australia/Perth': 1, }; for (const expectTimezone in expects) { const actual = timezones.find(tz => tz.timezone === expectTimezone); assert.equal(actual && Number(actual.count) === expects[expectTimezone], true, 'Timezone count was incorrect for ' + expectTimezone + ', expected ' + expects[expectTimezone] + ' but got ' + actual); } }); }); } ================================================ FILE: test/node/node-staking-tests.js ================================================ import { before, describe, it } from 'mocha'; import { RocketDAONodeTrustedSettingsMinipool, RocketDAOProtocolSettingsNode, RocketNodeStaking, StakeHelper, } from '../_utils/artifacts'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { nodeStakeRPL, nodeStakeRPLFor, registerNode, setNodeRPLWithdrawalAddress, setNodeTrusted, setNodeWithdrawalAddress, setRPLLockingAllowed, setStakeRPLForAllowed, setStakeRPLForAllowedWithNodeAddress, } from '../_helpers/node'; import { approveRPL, mintRPL } from '../_helpers/tokens'; import { stakeRpl } from './scenario-stake-rpl'; import { withdrawRpl, withdrawRplFor } from './scenario-withdraw-rpl'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { globalSnapShot, snapshotDescribe } from '../_utils/snapshotting'; import { unstakeRpl, unstakeRplFor } from './scenario-unstake-rpl'; import { assertBN } from '../_helpers/bn'; import { unstakeLegacyRpl, unstakeLegacyRplFor } from './scenario-unstake-legacy-rpl'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketNodeStaking', () => { let owner, node, node2, trustedNode, random, rplWithdrawalAddress, withdrawalAddress; const scrubPeriod = (60 * 60 * 24); // 24 hours const userDistributeStartTime = 60 * 60 * 24 * 90; // 90 days // Setup let rocketNodeStaking; before(async () => { await globalSnapShot(); [ owner, node, node2, trustedNode, random, rplWithdrawalAddress, withdrawalAddress, ] = await ethers.getSigners(); // Load contracts rocketNodeStaking = await RocketNodeStaking.deployed(); // Set settings await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.scrub.period', scrubPeriod, { from: owner }); // Register node await registerNode({ from: node }); await registerNode({ from: node2 }); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node1@home.com', owner); // Mint RPL to accounts const rplAmount = '10000'.ether; await mintRPL(owner, node, rplAmount); await mintRPL(owner, node2, rplAmount); await mintRPL(owner, random, rplAmount); await mintRPL(owner, rplWithdrawalAddress, rplAmount); await mintRPL(owner, withdrawalAddress, rplAmount); }); // Helpers async function assertBalances(node, legacy, megapool) { const legacyStakedRPL = await rocketNodeStaking.getNodeLegacyStakedRPL(node); const megapoolStakedRPL = await rocketNodeStaking.getNodeMegapoolStakedRPL(node); const stakedRPL = await rocketNodeStaking.getNodeStakedRPL(node); assertBN.equal(legacyStakedRPL, legacy); assertBN.equal(stakedRPL, legacy + megapool); assertBN.equal(megapoolStakedRPL, megapool); } async function assertUnstakingBalance(node, amount) { const unstakingRPL = await rocketNodeStaking.getNodeUnstakingRPL(node); assertBN.equal(unstakingRPL, amount); } it(printTitle('node operator', 'can stake RPL'), async () => { // Set parameters const rplAmount = '5000'.ether; // Approve transfer & stake RPL once await approveRPL(rocketNodeStaking.target, rplAmount, { from: node }); await stakeRpl(rplAmount, { from: node }); // Approve transfer & stake RPL twice await approveRPL(rocketNodeStaking.target, rplAmount, { from: node }); await stakeRpl(rplAmount, { from: node }); // Assert balances await assertBalances(node, 0n, rplAmount * 2n); }); it(printTitle('random address', 'cannot stake RPL'), async () => { // Set parameters const rplAmount = '10000'.ether; // Approve transfer & attempt to stake RPL await approveRPL(rocketNodeStaking.target, rplAmount, { from: node }); await shouldRevert( stakeRpl(rplAmount, { from: random }), 'Random address staked RPL', ); }); it(printTitle('node operator', 'cannot withdraw staked RPL'), async () => { // Set parameters const rplAmount = '10000'.ether; // Stake RPL await nodeStakeRPL(rplAmount, { from: node }); // Withdraw staked RPL await shouldRevert( withdrawRpl({ from: node }), 'Was able to withdraw RPL', 'No available unstaking RPL to withdraw', ); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('node operator', 'cannot unstake megapool RPL as legacy RPL'), async () => { const rplAmount = '100'.ether; // Stake 1000 megapool RPL await nodeStakeRPL(rplAmount, { from: node }); // Fail to unstake legacy RPL await shouldRevert( unstakeLegacyRpl(rplAmount, { from: node }), 'Was able to unstake legacy RPL', 'Insufficient legacy staked RPL', ); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('node operator', 'cannot unstake more RPL than staked'), async () => { // Stake 10 megapool RPL await nodeStakeRPL('10'.ether, { from: node }); // Try to unstake await shouldRevert(unstakeRpl('10000'.ether, { from: node, }), 'Was able to unstake more RPL than staked', 'Insufficient RPL stake to reduce'); // Assert balances await assertBalances(node, 0n, '10'.ether); }); it(printTitle('node operator', 'cannot unstake RPL that is locked'), async () => { // Stake megapool 100 RPL await nodeStakeRPL('100'.ether, { from: node }); // Lock 50 RPL const stakeHelper = await StakeHelper.deployed(); await setRPLLockingAllowed(node.address, true, { from: node }); await stakeHelper.lockRPL(node.address, '50'.ether); // Fail to unstake 100 megapool RPL await shouldRevert( unstakeRpl('100'.ether, { from: node }), 'Was able to unstake more than available', 'Insufficient RPL stake to reduce', ); // Unstake 50 megapool RPL await unstakeRpl('50'.ether, { from: node }); // Unlock the other 50 RPL await stakeHelper.unlockRPL(node.address, '50'.ether); // Unstake 50 megapool RPL await unstakeRpl('50'.ether, { from: node }); // Assert balances await assertBalances(node, 0n, 0n); }); it(printTitle('node operator', 'can unstake RPL'), async () => { // Stake 10,000 megapool RPL const rplAmount = '10000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Withdraw staked megapool RPL await unstakeRpl(rplAmount, { from: node }); // Assert balances await assertBalances(node, 0n, 0n); }); it(printTitle('node operator', 'can withdraw RPL during unstake if unstaking period has passed'), async () => { // Stake 5,000 megapool RPL await nodeStakeRPL('5000'.ether, { from: node }); // Unstake 1,000 RPL await unstakeRpl('1000'.ether, { from: node }); // Assert balances await assertBalances(node, 0n, '4000'.ether); await assertUnstakingBalance(node, '1000'.ether); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Unstake another 1,000 RPL await unstakeRpl('1000'.ether, { from: node }); // Assert balances await assertBalances(node, 0n, '3000'.ether); await assertUnstakingBalance(node, '1000'.ether); }); it(printTitle('node operator', 'can not withdraw RPL if unstaking period has passed, but withdrawal cooldown has not'), async () => { // Set cooldown to 5 days const withdrawalCooldown = 60 * 60 * 24 * 5; await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.withdrawal.cooldown', withdrawalCooldown, { from: owner }); // Stake 1,000 megapool RPL await nodeStakeRPL('1000'.ether, { from: node }); // Unstake 1,000 RPL await unstakeRpl('1000'.ether, { from: node }); // Assert balances await assertBalances(node, 0n, '0'.ether); await assertUnstakingBalance(node, '1000'.ether); // Wait 28 days (unstaking period) await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Stake another 1 RPL to reset cooldown await nodeStakeRPL('1'.ether, { from: node }); // Should be unable to withdraw due to cooldown await shouldRevert( withdrawRpl({ from: node }), 'Was able to withdraw before cooldown', 'No available unstaking RPL to withdraw' ); // Wait 5 days await helpers.time.increase(withdrawalCooldown + 1); // Should be able to withdraw now await withdrawRpl({ from: node }); }); it(printTitle('node operator', 'can unstake RPL from RPL withdrawal address'), async () => { // Stake 10,000 megapool RPL const rplAmount = '10000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Withdraw staked megapool RPL await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); await unstakeRplFor(rplAmount, node.address, rplWithdrawalAddress); // Assert balances await assertBalances(node, 0n, 0n); }); it(printTitle('random', 'can not unstake RPL for node operator'), async () => { // Stake 10,000 megapool RPL const rplAmount = '10000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Withdraw staked megapool RPL await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); await shouldRevert( unstakeRplFor(rplAmount, node.address, random), 'Unstaked with random account', 'Not allowed to unstake for' ); // Assert balances await assertBalances(rplWithdrawalAddress.address, 0n, 0n); }); it(printTitle('node operator', 'can withdraw unstaked RPL after waiting 28 days'), async () => { // Stake 10,000 megapool RPL await nodeStakeRPL('10000'.ether, { from: node }); // Unstake 500 megapool RPL await unstakeRpl('500'.ether, { from: node, }); // Fail to withdraw immediately await shouldRevert( withdrawRpl({ from: node }), 'Was able to immediately withdraw RPL', 'No available unstaking RPL to withdraw', ); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Can now withdraw the 500 megapool RPL await withdrawRpl({ from: node }); // Assert balances await assertBalances(node, 0n, '10000'.ether - '500'.ether); }); it(printTitle('node operator', 'can withdraw unstaked RPL from RPL withdrawal address after waiting 28 days'), async () => { // Stake 10,000 megapool RPL await nodeStakeRPL('10000'.ether, { from: node }); // Unstake 500 megapool RPL await unstakeRpl('500'.ether, { from: node, }); // Fail to withdraw immediately await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); await shouldRevert( withdrawRplFor(node, rplWithdrawalAddress), 'Was able to immediately withdraw RPL', 'No available unstaking RPL to withdraw', ); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Can now withdraw the 500 megapool RPL await withdrawRplFor(node, rplWithdrawalAddress, { from: node }); // Assert balances await assertBalances(node, 0n, '10000'.ether - '500'.ether); }); it(printTitle('random address', 'cannot stake on behalf of a node without allowance'), async () => { await shouldRevert( nodeStakeRPLFor(node, '10000'.ether, { from: random }), 'Was able to stake', 'Not allowed to stake for', ); }); it(printTitle('random address', 'can stake on behalf of a node with allowance'), async () => { // Set parameters const rplAmount = '10000'.ether; // Allow await setStakeRPLForAllowed(random, true, { from: node }); // Stake RPL await nodeStakeRPLFor(node, rplAmount, { from: random }); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('random address', 'can stake on behalf of a node with allowance from withdrawal address'), async () => { // Set parameters const rplAmount = '10000'.ether; // Set RPL withdrawal address await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); // Not allowed to set from node address any more await shouldRevert( setStakeRPLForAllowed(random, true, { from: node }), 'Was able to allow', 'Must be called from RPL withdrawal address', ); // Allow from RPL withdrawal address await setStakeRPLForAllowedWithNodeAddress(node, random, true, { from: rplWithdrawalAddress }); // Stake RPL await nodeStakeRPLFor(node, rplAmount, { from: random }); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('node operator', 'cannot stake from node address once RPL withdrawal address is set'), async () => { // Set RPL withdrawal address await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); // Stake RPL await shouldRevert( nodeStakeRPL('10000'.ether, { from: node }), 'Was able to stake', 'Not allowed to stake for', ); }); it(printTitle('node operator', 'can stake from primary withdrawal address'), async () => { // Set parameters const rplAmount = '10000'.ether; // Set RPL withdrawal address await setNodeWithdrawalAddress(node, withdrawalAddress, { from: node }); // Stake RPL await nodeStakeRPLFor(node, rplAmount, { from: withdrawalAddress }); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('node operator', 'can stake from RPL withdrawal address'), async () => { // Set parameters const rplAmount = '10000'.ether; // Set RPL withdrawal address await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); // Stake RPL await nodeStakeRPLFor(node, rplAmount, { from: rplWithdrawalAddress }); // Assert balances await assertBalances(node, 0n, rplAmount); }); it(printTitle('misc', 'can transfer amounts between nodes'), async () => { // Stake 100 RPL from both nodes const rplAmount = '100'.ether; await nodeStakeRPL(rplAmount, { from: node }); await nodeStakeRPL(rplAmount, { from: node2 }); // Transfer 50 from one to another const stakeHelper = await StakeHelper.deployed(); await stakeHelper.transferRPL(node.address, node2.address, '50'.ether); // Try to unstake 100 from first node await shouldRevert( unstakeRpl('100'.ether, { from: node }), 'Was able to unstake more RPL than staked', 'Insufficient RPL stake to reduce', ); // Unstake 50 megapool RPL from first node await unstakeRpl('50'.ether, { from: node }); // Unstake 150 megapool RPL from second node await unstakeRpl('150'.ether, { from: node2 }); // Assert balances await assertBalances(node, 0n, 0n); await assertBalances(node2, 0n, 0n); }); it(printTitle('misc', 'cannot lock unstaking RPL'), async () => { // Stake 100 RPL const rplAmount = '100'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Unstake 50 RPL await unstakeRpl('50'.ether, { from: node, }); // Try to lock 100 RPL const stakeHelper = await StakeHelper.deployed(); await setRPLLockingAllowed(node.address, true, { from: node }); await shouldRevert( stakeHelper.lockRPL(node.address, '100'.ether), 'Was able to lock unstaking RPL', 'Not enough staked RPL', ); // Try to lock 50 RPL await stakeHelper.lockRPL(node.address, '50'.ether); // Assert balances await assertBalances(node, 0n, '50'.ether); }); it(printTitle('misc', 'can burn RPL'), async () => { // Stake 100 RPL const rplAmount = '100'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Burn 50 RPL const stakeHelper = await StakeHelper.deployed(); await stakeHelper.burnRPL(node.address, '50'.ether); // Try to unstake 100 RPL await shouldRevert( unstakeRpl('100'.ether, { from: node }), 'Was able to unstake more RPL than staked', 'Insufficient RPL stake to reduce', ); // Try to unstake 50 RPL await unstakeRpl('50'.ether, { from: node, }); // Assert balances await assertBalances(node, 0n, 0n); }); snapshotDescribe('With 1000 legacy staked RPL', () => { const legacyAmount = '1000'.ether; before(async () => { // Add 100 RPL as legacy staked by node const stakeHelper = await StakeHelper.deployed(); await mintRPL(owner, owner, legacyAmount); await approveRPL(stakeHelper.target, legacyAmount, { from: owner }); await stakeHelper.addLegacyStakedRPL(node.address, legacyAmount); // Assert expected balances await assertBalances(node, legacyAmount, 0n); }); it(printTitle('node operator', 'can unstake and withdraw legacy RPL'), async () => { // Unstake 500 RPL await unstakeLegacyRpl('500'.ether, { from: node }); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Withdraw the 500 legacy staked await withdrawRpl({ from: node }); // Check await assertBalances(node, '500'.ether, 0n); }); it(printTitle('node operator', 'can withdraw legacy RPL from RPL withdrawal address'), async () => { // Set RPL withdrawal address await setNodeRPLWithdrawalAddress(node, rplWithdrawalAddress, { from: node }); // Unstake RPL await unstakeLegacyRplFor(legacyAmount, node, rplWithdrawalAddress); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Withdraw the 500 megapool staked await withdrawRplFor(node, rplWithdrawalAddress); }); it(printTitle('random', 'can not withdraw legacy RPL for node operator'), async () => { await shouldRevert( unstakeLegacyRplFor(legacyAmount, node.address, random), 'Was able to withdraw from random account', 'Not allowed to unstake legacy RPL for' ); }); it(printTitle('node operator', 'cannot unstake legacy staked RPL'), async () => { await shouldRevert( unstakeRpl('1'.ether, { from: node }), 'Was able to unstake legacy staked RPL', 'Insufficient RPL stake to reduce', ); }); it(printTitle('node operator', 'cannot withdraw legacy staked RPL as megapool staked RPL'), async () => { await shouldRevert( withdrawRpl({ from: node }), 'Was able to withdraw legacy staked RPL as megapool staked RPL', 'No available unstaking RPL to withdraw', ); }); it(printTitle('node operator', 'can stake megapool staked RPL and then unstake both'), async () => { // Stake 1000 megapool staked RPL await nodeStakeRPL('1000'.ether, { from: node }); // NO now has 1000 legacy and 1000 megapool // Unstake 500 megapool await unstakeRpl('500'.ether, { from: node }); // Unstake 500 legacy await unstakeLegacyRpl('500'.ether, { from: node }); // Wait 28 days await helpers.time.increase(60 * 60 * 24 * 28 + 1); // Withdraw the 500 megapool staked await withdrawRpl({ from: node }); // NO should now have 500 legacy and 500 megapool await assertBalances(node, '500'.ether, '500'.ether); }); it(printTitle('node operator', 'can withdraw unlocked legacy RPL'), async () => { // Stake 1000 RPL const rplAmount = '1000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Lock 1500 RPL const stakeHelper = await StakeHelper.deployed(); await setRPLLockingAllowed(node.address, true, { from: node }); await stakeHelper.lockRPL(node.address, '1500'.ether); // NO should have 1000 legacy staked RPL, 1000 megapool staked RPL, and 1500 locked RPL, leaving 500 left to unstake // Try to unstake 1000 legacy RPL await shouldRevert( unstakeLegacyRpl('1000'.ether, { from: node }), 'Was able to unstake more than available', 'Insufficient RPL stake to reduce', ); // Try to unstake 500 legacy RPL await unstakeLegacyRpl('500'.ether, { from: node }); // Fail to unstake 500 megapool RPL await shouldRevert( unstakeRpl('500'.ether, { from: node }), 'Was able to unstake megapool RPL', 'Insufficient RPL stake to reduce', ); // Unlock the RPL await stakeHelper.unlockRPL(node.address, '1500'.ether); // Unstake the RPL await unstakeRpl('500'.ether, { from: node }); // Should have 500 of each now await assertBalances(node, '500'.ether, '500'.ether); }); it(printTitle('node operator', 'can withdraw unlocked megapool RPL'), async () => { // Stake 1000 RPL const rplAmount = '1000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Lock 1500 RPL const stakeHelper = await StakeHelper.deployed(); await setRPLLockingAllowed(node.address, true, { from: node }); await stakeHelper.lockRPL(node.address, '1500'.ether); // NO should have 1000 legacy staked RPL, 1000 megapool staked RPL, and 1500 locked RPL, leaving 500 left to unstake // Fail to unstake 1000 megapool RPL await shouldRevert( unstakeRpl('1000'.ether, { from: node }), 'Was able to unstake more than available', 'Insufficient RPL stake to reduce', ); // Try to unstake 500 RPL await unstakeRpl('500'.ether, { from: node }); // Fail to unstake 500 legacy RPL await shouldRevert( unstakeLegacyRpl('500'.ether, { from: node }), 'Was able to unstake legacy RPL', 'Insufficient RPL stake to reduce', ); // Unlock the RPL await stakeHelper.unlockRPL(node.address, '1500'.ether); // Withdraw as legacy RPL await unstakeLegacyRpl('500'.ether, { from: node }); // Should have 500 of each now await assertBalances(node, '500'.ether, '500'.ether); }); it(printTitle('misc', 'can transfer a combination of legacy and megapool staked RPL between nodes'), async () => { // Stake 1000 RPL const rplAmount = '1000'.ether; await nodeStakeRPL(rplAmount, { from: node }); // Transfer 1500 RPL to another node, this should transfer 1000 legacy and 500 megapool const stakeHelper = await StakeHelper.deployed(); await stakeHelper.transferRPL(node.address, node2.address, '1500'.ether); // First node should have 500 megapool, second node should now have 1500 megapool await assertBalances(node, 0n, '500'.ether); await assertBalances(node2, 0n, '1500'.ether); }); }); }); } ================================================ FILE: test/node/scenario-deposit-v2.js ================================================ import { RocketMinipoolDelegate, RocketMinipoolFactory, RocketMinipoolManager, RocketNodeDeposit, } from '../_utils/artifacts'; import { getDepositDataRoot, getValidatorPubkey, getValidatorSignature } from '../_utils/beacon'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; let minipoolSalt = 0; // Make a node deposit export async function depositV2(minimumNodeFee, bondAmount, txOptions) { // Load contracts const [ rocketMinipoolManager, rocketMinipoolFactory, rocketNodeDeposit, ] = await Promise.all([ RocketMinipoolManager.deployed(), RocketMinipoolFactory.deployed(), RocketNodeDeposit.deployed(), ]); // Get minipool counts function getMinipoolCounts(nodeAddress) { return Promise.all([ rocketMinipoolManager.getMinipoolCount(), rocketMinipoolManager.getNodeMinipoolCount(nodeAddress), ]).then( ([network, node]) => ({network, node}) ); } // Get minipool details function getMinipoolDetails(minipoolAddress) { const minipool = RocketMinipoolDelegate.at(minipoolAddress); return Promise.all([ rocketMinipoolManager.getMinipoolExists(minipoolAddress), minipool.getNodeAddress(), minipool.getNodeDepositBalance(), minipool.getNodeDepositAssigned(), ]).then( ([exists, nodeAddress, nodeDepositBalance, nodeDepositAssigned]) => ({exists, nodeAddress, nodeDepositBalance, nodeDepositAssigned}) ); } // Get initial minipool indexes let minipoolCounts1 = await getMinipoolCounts(txOptions.from); // Deposit const salt = minipoolSalt++; const minipoolAddress = await rocketMinipoolFactory.getExpectedAddress(txOptions.from, salt); let withdrawalCredentials = '0x010000000000000000000000' + minipoolAddress.substr(2); // Get validator deposit data let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), amount: BigInt(1000000000), // 1 ETH in gwei signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); // Make node deposit if (bondAmount === txOptions.value) { await rocketNodeDeposit.connect(txOptions.from).deposit(bondAmount, minimumNodeFee, depositData.pubkey, depositData.signature, depositDataRoot, salt, minipoolAddress, txOptions); } else { await rocketNodeDeposit.connect(txOptions.from).depositWithCredit(bondAmount, minimumNodeFee, depositData.pubkey, depositData.signature, depositDataRoot, salt, minipoolAddress, txOptions); } // Get updated minipool indexes & created minipool details let minipoolCounts2 = await getMinipoolCounts(txOptions.from); let [ lastMinipoolAddress, lastNodeMinipoolAddress, minipoolDetails, ] = await Promise.all([ rocketMinipoolManager.getMinipoolAt(minipoolCounts2.network - 1n), rocketMinipoolManager.getNodeMinipoolAt(txOptions.from, minipoolCounts2.node - 1n), getMinipoolDetails(minipoolAddress), ]); // Check minipool indexes assertBN.equal(minipoolCounts2.network, minipoolCounts1.network + 1n, 'Incorrect updated network minipool count'); assert.strictEqual(lastMinipoolAddress.toLowerCase(), minipoolAddress.toLowerCase(), 'Incorrect updated network minipool index'); assertBN.equal(minipoolCounts2.node, minipoolCounts1.node + 1n, 'Incorrect updated node minipool count'); assert.strictEqual(lastNodeMinipoolAddress.toLowerCase(), minipoolAddress.toLowerCase(), 'Incorrect updated node minipool index'); // Check minipool details assert.equal(minipoolDetails.exists, true, 'Incorrect created minipool exists status'); assert.strictEqual(minipoolDetails.nodeAddress.toLowerCase(), txOptions.from.address.toLowerCase(), 'Incorrect created minipool node address'); assertBN.equal(minipoolDetails.nodeDepositBalance, bondAmount, 'Incorrect created minipool node deposit balance'); return minipoolAddress } ================================================ FILE: test/node/scenario-distribute-rewards.js ================================================ import { RocketMinipoolDelegate, RocketMinipoolManager, RocketNodeDistributorDelegate, RocketNodeDistributorFactory, RocketNodeManager, RocketStorage, RocketTokenRETH, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; export async function distributeRewards(nodeAddress, txOptions) { // Get contracts const rocketStorage = await RocketStorage.deployed(); const rocketNodeDistributorFactory = await RocketNodeDistributorFactory.deployed(); const distributorAddress = await rocketNodeDistributorFactory.getProxyAddress(nodeAddress); const distributor = await RocketNodeDistributorDelegate.at(distributorAddress); const rocketTokenRETH = await RocketTokenRETH.deployed(); const rocketMinipoolManager = await RocketMinipoolManager.deployed(); const rocketNodeManager = await RocketNodeManager.deployed(); // Get node withdrawal address const withdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(nodeAddress); // Get distributor contract balance const distributorBalance = await ethers.provider.getBalance(distributorAddress); // Get nodes average fee const minipoolCount = Number(await rocketMinipoolManager.getNodeMinipoolCount(nodeAddress)); async function getMinipoolDetails(index) { const minipoolAddress = await rocketMinipoolManager.getNodeMinipoolAt(nodeAddress, index); const minipool = await RocketMinipoolDelegate.at(minipoolAddress); return Promise.all([ minipool.getStatus(), minipool.getNodeFee(), ]).then( ([status, fee]) => ({ status: Number(status), fee, }), ); } // Get status and node fee of each minipool const minipoolDetails = await Promise.all([...Array(minipoolCount).keys()].map(i => getMinipoolDetails(i))); let numerator = 0n; let denominator = 0n; for (const minipoolDetail of minipoolDetails) { if (minipoolDetail.status === 2) { // Staking numerator = numerator + minipoolDetail.fee; denominator = denominator + 1n; } } let expectedAverageFee = 0n; if (numerator !== 0n) { expectedAverageFee = numerator / denominator; } // Query average fee from contracts const averageFee = await rocketNodeManager.getAverageNodeFee(nodeAddress.address); assertBN.equal(averageFee, expectedAverageFee, 'Incorrect average node fee'); // Calculate expected node and user amounts from average fee const halfAmount = distributorBalance / 2n; const expectedNodeAmount = halfAmount + (halfAmount * averageFee / '1'.ether); const expectedUserAmount = distributorBalance - expectedNodeAmount; async function getBalances() { return Promise.all([ ethers.provider.getBalance(withdrawalAddress), ethers.provider.getBalance(rocketTokenRETH.target), rocketNodeManager.getUnclaimedRewards(nodeAddress), ]).then( ([nodeEth, userEth, unclaimedEth]) => ({ nodeEth, userEth, unclaimedEth }), ); } // Get balance before distribute const balances1 = await getBalances(); // Call distributor await distributor.connect(txOptions.from).distribute(); // Get balance after distribute const balances2 = await getBalances(); // Check results const nodeEthChange = (balances2.nodeEth + balances2.unclaimedEth) - (balances1.nodeEth + balances1.unclaimedEth); const userEthChange = balances2.userEth - balances1.userEth; assertBN.equal(nodeEthChange, expectedNodeAmount, 'Node ETH balance not correct'); assertBN.equal(userEthChange, expectedUserAmount, 'User ETH balance not correct'); } ================================================ FILE: test/node/scenario-register-smoothing-pool.js ================================================ import { RocketNodeManager } from '../_utils/artifacts'; import * as assert from 'assert'; // Register a node export async function setSmoothingPoolRegistrationState(state, txOptions) { // Load contracts const rocketNodeManager = await RocketNodeManager.deployed(); // Register await rocketNodeManager.connect(txOptions.from).setSmoothingPoolRegistrationState(state, txOptions); // Check details const newState = await rocketNodeManager.getSmoothingPoolRegistrationState(txOptions.from.address); assert.strictEqual(newState, state, 'Incorrect smoothing pool registration state'); } ================================================ FILE: test/node/scenario-register.js ================================================ import { RocketNodeManager } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; // Register a node export async function register(timezoneLocation, txOptions) { // Load contracts const rocketNodeManager = await RocketNodeManager.deployed(); // Get node details function getNodeDetails(nodeAddress) { return Promise.all([ rocketNodeManager.getNodeExists(nodeAddress), rocketNodeManager.getNodeTimezoneLocation(nodeAddress), ]).then( ([exists, timezoneLocation]) => ({ exists, timezoneLocation }), ); } // Get initial node index let nodeCount1 = await rocketNodeManager.getNodeCount(); // Register await rocketNodeManager.connect(txOptions.from).registerNode(timezoneLocation, txOptions); // Get updated node index & node details let nodeCount2 = await rocketNodeManager.getNodeCount(); let [lastNodeAddress, details] = await Promise.all([ rocketNodeManager.getNodeAt(nodeCount2 - 1n), getNodeDetails(txOptions.from.address), ]); // Check details assertBN.equal(nodeCount2, nodeCount1 + 1n, 'Incorrect updated node count'); assert.strictEqual(lastNodeAddress, txOptions.from.address, 'Incorrect updated node index'); assert.equal(details.exists, true, 'Incorrect node exists flag'); assert.strictEqual(details.timezoneLocation, timezoneLocation, 'Incorrect node timezone location'); } ================================================ FILE: test/node/scenario-set-timezone.js ================================================ import { RocketNodeManager } from '../../test/_utils/artifacts'; import * as assert from 'assert'; // Set a node's timezone location export async function setTimezoneLocation(timezoneLocation, txOptions) { // Load contracts const rocketNodeManager = await RocketNodeManager.deployed(); // Set timezone location await rocketNodeManager.connect(txOptions.from).setTimezoneLocation(timezoneLocation, txOptions); // Get timezone location let nodeTimezoneLocation = await rocketNodeManager.getNodeTimezoneLocation(txOptions.from.address); // Check assert.strictEqual(nodeTimezoneLocation, timezoneLocation, 'Incorrect updated timezone location'); } ================================================ FILE: test/node/scenario-set-withdrawal-address.js ================================================ import { RocketStorage } from '../../test/_utils/artifacts'; import * as assert from 'assert'; // Set a node's withdrawal address export async function setWithdrawalAddress(nodeAddress, withdrawalAddress, confirm, txOptions) { // Load contracts const rocketStorage = await RocketStorage.deployed(); // Set withdrawal address await rocketStorage.connect(txOptions.from).setWithdrawalAddress(nodeAddress, withdrawalAddress, confirm, txOptions); // Get current & pending withdrawal addresses let nodeWithdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(nodeAddress); let nodePendingWithdrawalAddress = await rocketStorage.getNodePendingWithdrawalAddress(nodeAddress); // Confirmed update check if (confirm) { assert.strictEqual(nodeWithdrawalAddress, withdrawalAddress, 'Incorrect updated withdrawal address'); } // Unconfirmed update check else { assert.strictEqual(nodePendingWithdrawalAddress, withdrawalAddress, 'Incorrect updated pending withdrawal address'); } } // Confirm a node's net withdrawal address export async function confirmWithdrawalAddress(nodeAddress, txOptions) { // Load contracts const rocketStorage = await RocketStorage.deployed(); // Confirm withdrawal address await rocketStorage.connect(txOptions.from).confirmWithdrawalAddress(nodeAddress, txOptions); // Get current & pending withdrawal addresses let nodeWithdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(nodeAddress); let nodePendingWithdrawalAddress = await rocketStorage.getNodePendingWithdrawalAddress(nodeAddress); // Check assert.strictEqual(nodeWithdrawalAddress, txOptions.from.address, 'Incorrect updated withdrawal address'); assert.strictEqual(nodePendingWithdrawalAddress, '0x0000000000000000000000000000000000000000', 'Incorrect pending withdrawal address'); } ================================================ FILE: test/node/scenario-stake-rpl.js ================================================ import { RocketNodeStaking, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Stake megapool RPL export async function stakeRpl(amount, txOptions) { // Load contracts const [ rocketNodeStaking, rocketTokenRPL, rocketVault, ] = await Promise.all([ RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), RocketVault.deployed(), ]); const nodeAddress = txOptions.from.address; async function getData(){ return Promise.all([ rocketTokenRPL.balanceOf(nodeAddress), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketNodeStaking', rocketTokenRPL.target), rocketNodeStaking.getTotalStakedRPL(), rocketNodeStaking.getTotalMegapoolStakedRPL(), rocketNodeStaking.getTotalLegacyStakedRPL(), rocketNodeStaking.getNodeStakedRPL(nodeAddress), rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress), rocketNodeStaking.getNodeLegacyStakedRPL(nodeAddress), ]).then( ([nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl]) => ({ nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl }), ); } const data1 = await getData(); await rocketNodeStaking.connect(txOptions.from).stakeRPL(amount, txOptions); const data2 = await getData(); const deltas = { nodeRpl: data2.nodeRpl - data1.nodeRpl, vaultRpl: data2.vaultRpl - data1.vaultRpl, stakingRpl: data2.stakingRpl - data1.stakingRpl, totalStakedRpl: data2.totalStakedRpl - data1.totalStakedRpl, totalMegapoolRpl: data2.totalMegapoolRpl- data1.totalMegapoolRpl, totalLegacyRpl: data2.totalLegacyRpl - data1.totalLegacyRpl, nodeStakedRpl: data2.nodeStakedRpl - data1.nodeStakedRpl, nodeMegapoolRpl: data2.nodeMegapoolRpl - data1.nodeMegapoolRpl, nodeLegacyRpl: data2.nodeLegacyRpl - data1.nodeLegacyRpl, } // Staking should transfer RPL from node to vault assertBN.equal(deltas.nodeRpl, -amount); assertBN.equal(deltas.vaultRpl, amount); assertBN.equal(deltas.stakingRpl, amount); // Unstaking immediately reduces "staked" RPL balances assertBN.equal(deltas.totalStakedRpl, amount); assertBN.equal(deltas.totalMegapoolRpl, amount); assertBN.equal(deltas.totalLegacyRpl, 0); assertBN.equal(deltas.nodeStakedRpl, amount); assertBN.equal(deltas.nodeMegapoolRpl, amount); assertBN.equal(deltas.nodeLegacyRpl, 0); } ================================================ FILE: test/node/scenario-unstake-legacy-rpl.js ================================================ import { RocketNodeManager, RocketNodeStaking, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Unstake legacy RPL for export async function unstakeLegacyRpl(amount, txOptions) { return unstakeLegacyRplFor(amount, txOptions.from.address, txOptions.from) } // Unstake legacy RPL export async function unstakeLegacyRplFor(amount, nodeAddress, from) { // Load contracts const [ rocketNodeStaking, rocketTokenRPL, rocketVault, rocketNodeManager, ] = await Promise.all([ RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), RocketVault.deployed(), RocketNodeManager.deployed(), ]); const rplWithdrawalAddress = await rocketNodeManager.getNodeRPLWithdrawalAddress(nodeAddress); async function getData(){ return Promise.all([ rocketTokenRPL.balanceOf(rplWithdrawalAddress), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketNodeStaking', rocketTokenRPL.target), rocketNodeStaking.getTotalStakedRPL(), rocketNodeStaking.getTotalMegapoolStakedRPL(), rocketNodeStaking.getTotalLegacyStakedRPL(), rocketNodeStaking.getNodeStakedRPL(nodeAddress), rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress), rocketNodeStaking.getNodeLegacyStakedRPL(nodeAddress), rocketNodeStaking.getNodeLastUnstakeTime(nodeAddress), rocketNodeStaking.getNodeUnstakingRPL(nodeAddress), ]).then( ([nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl ]) => ({ nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl }), ); } const data1 = await getData(); let tx if (from.address === nodeAddress) { tx = await rocketNodeStaking.connect(from).unstakeLegacyRPL(amount); } else { tx = await rocketNodeStaking.connect(from).unstakeLegacyRPLFor(nodeAddress, amount); } const block = await ethers.provider.getBlock(tx.blockNumber); const txTimestamp = block.timestamp; const data2 = await getData(); const deltas = { nodeRpl: data2.nodeRpl - data1.nodeRpl, vaultRpl: data2.vaultRpl - data1.vaultRpl, stakingRpl: data2.stakingRpl - data1.stakingRpl, totalStakedRpl: data2.totalStakedRpl - data1.totalStakedRpl, totalMegapoolRpl: data2.totalMegapoolRpl- data1.totalMegapoolRpl, totalLegacyRpl: data2.totalLegacyRpl - data1.totalLegacyRpl, nodeStakedRpl: data2.nodeStakedRpl - data1.nodeStakedRpl, nodeMegapoolRpl: data2.nodeMegapoolRpl - data1.nodeMegapoolRpl, nodeLegacyRpl: data2.nodeLegacyRpl - data1.nodeLegacyRpl, nodeUnstakingRpl: data2.nodeUnstakingRpl - data1.nodeUnstakingRpl, } // Last unstake time should be set to transaction timestamp assertBN.equal(data2.nodeLastUnstakeTime, txTimestamp) // If last unstake was longer than 28 days ago, it should trigger a withdrawal let expectedWithdrawAmount = 0n if (txTimestamp - Number(data1.nodeLastUnstakeTime) > (60 * 60 * 24 * 28)) { expectedWithdrawAmount = data1.nodeUnstakingRpl } // Check RPL balances assertBN.equal(deltas.nodeRpl, expectedWithdrawAmount); assertBN.equal(deltas.vaultRpl, -expectedWithdrawAmount); assertBN.equal(deltas.stakingRpl, -expectedWithdrawAmount); // Unstaking immediately reduces "staked" RPL balances assertBN.equal(deltas.totalStakedRpl, -amount); assertBN.equal(deltas.totalMegapoolRpl, 0n); assertBN.equal(deltas.totalLegacyRpl, -amount); assertBN.equal(deltas.nodeStakedRpl, -amount); assertBN.equal(deltas.nodeMegapoolRpl, 0n); assertBN.equal(deltas.nodeLegacyRpl, -amount); // Unstaking balance should increase assertBN.equal(deltas.nodeUnstakingRpl, amount - expectedWithdrawAmount); } ================================================ FILE: test/node/scenario-unstake-rpl.js ================================================ import { RocketNodeManager, RocketNodeStaking, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; export async function unstakeRpl(amount, txOptions) { return unstakeRplFor(amount, txOptions.from.address, txOptions.from) } // Unstake megapool RPL export async function unstakeRplFor(amount, nodeAddress, from) { // Load contracts const [ rocketNodeManager, rocketNodeStaking, rocketTokenRPL, rocketVault, ] = await Promise.all([ RocketNodeManager.deployed(), RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), RocketVault.deployed(), ]); const rplWithdrawalAddress = await rocketNodeManager.getNodeRPLWithdrawalAddress(nodeAddress); async function getData(){ return Promise.all([ rocketTokenRPL.balanceOf(rplWithdrawalAddress), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketNodeStaking', rocketTokenRPL.target), rocketNodeStaking.getTotalStakedRPL(), rocketNodeStaking.getTotalMegapoolStakedRPL(), rocketNodeStaking.getTotalLegacyStakedRPL(), rocketNodeStaking.getNodeStakedRPL(nodeAddress), rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress), rocketNodeStaking.getNodeLegacyStakedRPL(nodeAddress), rocketNodeStaking.getNodeLastUnstakeTime(nodeAddress), rocketNodeStaking.getNodeUnstakingRPL(nodeAddress), ]).then( ([nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl ]) => ({ nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl }), ); } const data1 = await getData(); let tx if (from.address === nodeAddress) { tx = await rocketNodeStaking.connect(from).unstakeRPL(amount); } else { tx = await rocketNodeStaking.connect(from).unstakeRPLFor(nodeAddress, amount); } const block = await ethers.provider.getBlock(tx.blockNumber); const txTimestamp = block.timestamp; const data2 = await getData(); const deltas = { nodeRpl: data2.nodeRpl - data1.nodeRpl, vaultRpl: data2.vaultRpl - data1.vaultRpl, stakingRpl: data2.stakingRpl - data1.stakingRpl, totalStakedRpl: data2.totalStakedRpl - data1.totalStakedRpl, totalMegapoolRpl: data2.totalMegapoolRpl- data1.totalMegapoolRpl, totalLegacyRpl: data2.totalLegacyRpl - data1.totalLegacyRpl, nodeStakedRpl: data2.nodeStakedRpl - data1.nodeStakedRpl, nodeMegapoolRpl: data2.nodeMegapoolRpl - data1.nodeMegapoolRpl, nodeLegacyRpl: data2.nodeLegacyRpl - data1.nodeLegacyRpl, nodeUnstakingRpl: data2.nodeUnstakingRpl - data1.nodeUnstakingRpl, } // Last unstake time should be set to transaction timestamp assertBN.equal(data2.nodeLastUnstakeTime, txTimestamp) // If last unstake was longer than 28 days ago, it should trigger a withdrawal let expectedWithdrawAmount = 0n if (txTimestamp - Number(data1.nodeLastUnstakeTime) > (60 * 60 * 24 * 28)) { expectedWithdrawAmount = data1.nodeUnstakingRpl } // Check RPL balances assertBN.equal(deltas.nodeRpl, expectedWithdrawAmount); assertBN.equal(deltas.vaultRpl, -expectedWithdrawAmount); assertBN.equal(deltas.stakingRpl, -expectedWithdrawAmount); // Unstaking immediately reduces "staked" RPL balances assertBN.equal(deltas.totalStakedRpl, -amount); assertBN.equal(deltas.totalMegapoolRpl, -amount); assertBN.equal(deltas.totalLegacyRpl, 0n); assertBN.equal(deltas.nodeStakedRpl, -amount); assertBN.equal(deltas.nodeMegapoolRpl, -amount); assertBN.equal(deltas.nodeLegacyRpl, 0n); // Unstaking balance should increase assertBN.equal(deltas.nodeUnstakingRpl, amount - expectedWithdrawAmount); } ================================================ FILE: test/node/scenario-withdraw-rpl.js ================================================ import { RocketNodeManager, RocketNodeStaking, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; export async function withdrawRpl(txOptions) { return withdrawRplFor(txOptions.from.address, txOptions.from) } // Withdraw unstaking megapool RPL export async function withdrawRplFor(nodeAddress, from) { // Load contracts const [ rocketNodeStaking, rocketTokenRPL, rocketVault, rocketNodeManager, ] = await Promise.all([ RocketNodeStaking.deployed(), RocketTokenRPL.deployed(), RocketVault.deployed(), RocketNodeManager.deployed(), ]); const rplWithdrawalAddress = await rocketNodeManager.getNodeRPLWithdrawalAddress(nodeAddress); async function getData(){ return Promise.all([ rocketTokenRPL.balanceOf(rplWithdrawalAddress), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketNodeStaking', rocketTokenRPL.target), rocketNodeStaking.getTotalStakedRPL(), rocketNodeStaking.getTotalMegapoolStakedRPL(), rocketNodeStaking.getTotalLegacyStakedRPL(), rocketNodeStaking.getNodeStakedRPL(nodeAddress), rocketNodeStaking.getNodeMegapoolStakedRPL(nodeAddress), rocketNodeStaking.getNodeLegacyStakedRPL(nodeAddress), rocketNodeStaking.getNodeLastUnstakeTime(nodeAddress), rocketNodeStaking.getNodeUnstakingRPL(nodeAddress), ]).then( ([nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl ]) => ({ nodeRpl, vaultRpl, stakingRpl, totalStakedRpl, totalMegapoolRpl, totalLegacyRpl, nodeStakedRpl, nodeMegapoolRpl, nodeLegacyRpl, nodeLastUnstakeTime, nodeUnstakingRpl }), ); } const data1 = await getData(); if (from.address === nodeAddress) { await rocketNodeStaking.connect(from).withdrawRPL(); } else { await rocketNodeStaking.connect(from).withdrawRPLFor(nodeAddress); } const data2 = await getData(); const deltas = { nodeRpl: data2.nodeRpl - data1.nodeRpl, vaultRpl: data2.vaultRpl - data1.vaultRpl, stakingRpl: data2.stakingRpl - data1.stakingRpl, totalStakedRpl: data2.totalStakedRpl - data1.totalStakedRpl, totalMegapoolRpl: data2.totalMegapoolRpl- data1.totalMegapoolRpl, totalLegacyRpl: data2.totalLegacyRpl - data1.totalLegacyRpl, nodeStakedRpl: data2.nodeStakedRpl - data1.nodeStakedRpl, nodeMegapoolRpl: data2.nodeMegapoolRpl - data1.nodeMegapoolRpl, nodeLegacyRpl: data2.nodeLegacyRpl - data1.nodeLegacyRpl, } // Withdrawing should transfer RPL from vault to node const amount = data1.nodeUnstakingRpl assertBN.equal(data2.nodeUnstakingRpl, 0n); assertBN.equal(deltas.nodeRpl, amount); assertBN.equal(deltas.vaultRpl, -amount); assertBN.equal(deltas.stakingRpl, -amount); // Withdrawing has no affect on staking balances assertBN.equal(deltas.totalStakedRpl, 0n); assertBN.equal(deltas.totalMegapoolRpl, 0n); assertBN.equal(deltas.totalLegacyRpl, 0n); assertBN.equal(deltas.nodeStakedRpl, 0n); assertBN.equal(deltas.nodeMegapoolRpl, 0n); assertBN.equal(deltas.nodeLegacyRpl, 0n); } ================================================ FILE: test/rewards/rewards-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { submitPrices } from '../_helpers/network'; import { nodeStakeRPL, registerNode, setNodeRPLWithdrawalAddress, setNodeTrusted, setNodeWithdrawalAddress, } from '../_helpers/node'; import { RevertOnTransfer, RocketDAONodeTrustedProposals, RocketDAOProtocolSettingsNode, RocketMerkleDistributorMainnet, RocketRewardsPool, RocketSmoothingPool, RocketStorage, } from '../_utils/artifacts'; import { setDAONetworkBootstrapRewardsClaimers, setDAOProtocolBootstrapSetting, setRewardsClaimIntervalTime, setRPLInflationIntervalRate, setRPLInflationStartTime, } from '../dao/scenario-dao-protocol-bootstrap'; import { mintRPL } from '../_helpers/tokens'; import { executeRewards, submitRewards } from './scenario-submit-rewards'; import { claimRewards } from './scenario-claim-rewards'; import { claimAndStakeRewards } from './scenario-claim-and-stake-rewards'; import { parseRewardsMap } from '../_utils/merkle-tree'; import { daoNodeTrustedExecute, daoNodeTrustedPropose, daoNodeTrustedVote } from '../dao/scenario-dao-node-trusted'; import { getDAOProposalStartTime } from '../dao/scenario-dao-proposal'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; import * as assert from 'node:assert'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketRewardsPool', () => { let owner, userOne, registeredNode1, registeredNode2, registeredNodeTrusted1, registeredNodeTrusted2, unregisteredNodeTrusted1, unregisteredNodeTrusted2, node1WithdrawalAddress, node1RplWithdrawalAddress, random; // Constants const ONE_DAY = 24 * 60 * 60; const claimIntervalTime = BigInt(ONE_DAY * 28); const scrubPeriod = ONE_DAY; // Set some RPL inflation scenes let rplInflationSetup = async function() { // Current time let currentTime = await helpers.time.latest(); // Starting block for when inflation will begin let timeStart = currentTime + ONE_DAY; // Yearly inflation target let yearlyInflationTarget = 0.05; // Set the daily inflation start time await setRPLInflationStartTime(timeStart, { from: owner }); // Set the daily inflation rate await setRPLInflationIntervalRate(yearlyInflationTarget, { from: owner }); // claimIntervalTime must be greater than rewardIntervalTime for tests to properly function assertBN.isAbove(claimIntervalTime, ONE_DAY, 'Tests will not function correctly unless claimIntervalTime is greater than inflation period (1 day)'); // Return the starting time for inflation when it will be available return BigInt(timeStart + ONE_DAY); }; // Set a rewards claiming contract let rewardsContractSetup = async function(_trustedNodePerc, _protocolPerc, _nodePerc, _claimAmountPerc) { // Set the amount this contract can claim await setDAONetworkBootstrapRewardsClaimers(_trustedNodePerc, _protocolPerc, _nodePerc, { from: owner }); // Set the claim interval blocks await setRewardsClaimIntervalTime(claimIntervalTime, { from: owner }); }; async function kickTrustedNode(nodeAddress, voters) { let rocketDAONodeTrustedProposals = await RocketDAONodeTrustedProposals.deployed(); let proposalCalldata = rocketDAONodeTrustedProposals.interface.encodeFunctionData('proposalKick', [nodeAddress.address, 0n]); // Add the proposal let proposalID = await daoNodeTrustedPropose(`Kick ${nodeAddress.address}`, proposalCalldata, { from: registeredNodeTrusted1, }); // Current time let timeCurrent = await helpers.time.latest(); // Now increase time until the proposal is 'active' and can be voted on await helpers.time.increase((await getDAOProposalStartTime(proposalID) - timeCurrent) + 2); // Now lets vote for (const voter of voters) { await daoNodeTrustedVote(proposalID, true, { from: voter }); } // Proposal has passed, lets execute it now await daoNodeTrustedExecute(proposalID, { from: registeredNode1 }); } // Setup before(async () => { await globalSnapShot(); [ owner, userOne, registeredNode1, registeredNode2, registeredNodeTrusted1, registeredNodeTrusted2, unregisteredNodeTrusted1, unregisteredNodeTrusted2, node1WithdrawalAddress, node1RplWithdrawalAddress, random, ] = await ethers.getSigners(); let slotTimestamp = '1600000000'; // Register nodes await registerNode({ from: registeredNode1 }); await registerNode({ from: registeredNode2 }); await registerNode({ from: registeredNodeTrusted1 }); await registerNode({ from: registeredNodeTrusted2 }); await registerNode({ from: unregisteredNodeTrusted1 }); await registerNode({ from: unregisteredNodeTrusted2 }); // Set node 1 withdrawal address await setNodeWithdrawalAddress(registeredNode1, node1WithdrawalAddress, { from: registeredNode1 }); // Set nodes as trusted await setNodeTrusted(registeredNodeTrusted1, 'saas_1', 'node@home.com', owner); await setNodeTrusted(registeredNodeTrusted2, 'saas_2', 'node@home.com', owner); // Set max per-minipool stake to 100% and RPL price to 1 ether const block = await ethers.provider.getBlockNumber(); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.per.minipool.stake.maximum', '1'.ether, { from: owner }); await submitPrices(block, slotTimestamp, '1'.ether, { from: registeredNodeTrusted1 }); await submitPrices(block, slotTimestamp, '1'.ether, { from: registeredNodeTrusted2 }); // Mint and stake RPL await mintRPL(owner, registeredNode1, '32'.ether); await mintRPL(owner, registeredNode2, '32'.ether); await nodeStakeRPL('32'.ether, { from: registeredNode1 }); await nodeStakeRPL('32'.ether, { from: registeredNode2 }); }); /*** Setting Claimers *************************/ it(printTitle('userOne', 'fails to set interval blocks for rewards claim period'), async () => { // Set the rewards claims interval in seconds await shouldRevert(setRewardsClaimIntervalTime(100, { from: userOne, }), 'Non owner set interval blocks for rewards claim period'); }); it(printTitle('guardian', 'succeeds setting interval blocks for rewards claim period'), async () => { // Set the rewards claims interval in blocks await setRewardsClaimIntervalTime(100, { from: owner, }); }); it(printTitle('userOne', 'fails to set contract claimer percentage for rewards'), async () => { // Set the amount this contract can claim await shouldRevert(setDAONetworkBootstrapRewardsClaimers('0.1'.ether, '0.5'.ether, '0.4'.ether, { from: userOne, }), 'Non owner set contract claimer percentage for rewards'); }); it(printTitle('guardian', 'set contract claimer percentage for rewards, then update it'), async () => { // Set the amount this contract can claim await setDAONetworkBootstrapRewardsClaimers('0.1'.ether, '0.1'.ether, '0.8'.ether, { from: owner, }); }); it(printTitle('guardian', 'fails to set contract claimer percentages to lower than 100% total'), async () => { // Set the amount this contract can claim await shouldRevert(setDAONetworkBootstrapRewardsClaimers('0.1'.ether, '0.1'.ether, '0.1'.ether, { from: owner, }), 'Percentages were updated', 'Total does not equal 100%'); }); it(printTitle('guardian', 'fails to set contract claimer percentages to greater than 100% total'), async () => { // Set the amount this contract can claim await shouldRevert(setDAONetworkBootstrapRewardsClaimers('0.4'.ether, '0.4'.ether, '0.4'.ether, { from: owner, }), 'Percentages were updated', 'Total does not equal 100%'); }); /*** Trusted Nodes *************************/ it(printTitle('trusted node', 'can allocate ETH from smoothing pool to pDAO'), async () => { const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '0'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; const rocketSmoothingPool = await RocketSmoothingPool.deployed(); const rocketRewardsPool = await RocketRewardsPool.deployed(); // Send 0.5 ETH to smoothing pool await owner.sendTransaction({ to: rocketSmoothingPool.target, value: '0.5'.ether, }); // Send 0.5 ETH to rewards pool as "voter share" await rocketRewardsPool.depositVoterShare({ value: '0.5'.ether }) // Submit reward submission with 1 ETH to treasury await submitRewards(0, rewards, '0'.ether, '0'.ether, '1'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '1'.ether, { from: registeredNodeTrusted2 }); }) /*** Regular Nodes *************************/ it(printTitle('node', 'can claim RPL and ETH'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Send ETH to rewards pool const rocketSmoothingPool = await RocketSmoothingPool.deployed(); await owner.sendTransaction({ to: rocketSmoothingPool.target, value: '20'.ether, }); const rocketRewardsPool = await RocketRewardsPool.deployed(); const pendingRewards = await rocketRewardsPool.getPendingETHRewards.call(); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, { address: registeredNode2.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '2'.ether, nodeETH: '1'.ether, voterETH: 0n }, { address: registeredNodeTrusted1.address, network: 0, trustedNodeRPL: '1'.ether, nodeRPL: '2'.ether, nodeETH: '0'.ether, voterETH: 0n }, { address: userOne.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1.333'.ether, nodeETH: '0.3'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '2'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '2'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimRewards(registeredNode1.address, [0], [rewards], { from: registeredNode1, }); await claimRewards(registeredNode2.address, [0], [rewards], { from: registeredNode2, }); await claimRewards(registeredNodeTrusted1.address, [0], [rewards], { from: registeredNodeTrusted1, }); await claimRewards(userOne.address, [0], [rewards], { from: userOne, }); // Do a second claim interval await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimRewards(registeredNode1.address, [1], [rewards], { from: registeredNode1, }); await claimRewards(registeredNode2.address, [1], [rewards], { from: registeredNode2, }); }); it(printTitle('node', 'can claim from withdrawal address'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Send ETH to rewards pool const rocketSmoothingPool = await RocketSmoothingPool.deployed(); await owner.sendTransaction({ to: rocketSmoothingPool.target, value: '20'.ether, }); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimRewards(registeredNode1.address, [0], [rewards], { from: node1WithdrawalAddress, }); }); it(printTitle('node', 'can claim voter ETH to RPL withdrawal address'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Send ETH to rewards pool const rocketSmoothingPool = await RocketSmoothingPool.deployed(); await owner.sendTransaction({ to: rocketSmoothingPool.target, value: '20'.ether, }); // Set node's RPL withdrawal address await setNodeRPLWithdrawalAddress(registeredNode1, node1RplWithdrawalAddress, { from: node1WithdrawalAddress }); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '0'.ether, nodeETH: '1'.ether, voterETH: '2'.ether, }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim await claimRewards(registeredNode1.address, [0], [rewards], { from: node1WithdrawalAddress, }); }); it(printTitle('node', 'can not claim with invalid proof'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; // Create 3 snapshots await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); let treeData = parseRewardsMap(rewards); let proof = treeData.proof.claims[ethers.getAddress(registeredNode1.address)]; let rocketMerkleDistributorMainnet = await RocketMerkleDistributorMainnet.deployed(); // Attempt to claim reward for registeredNode1 with registeredNode2 const claim = { rewardIndex: 0, amountRPL: proof.amountRPL, amountSmoothingPoolETH: proof.amountSmoothingPoolETH, amountVoterETH: proof.amountVoterETH, merkleProof: proof.proof } await shouldRevert(rocketMerkleDistributorMainnet.connect(registeredNode2).claim(registeredNode2, [claim], { from: registeredNode2 }), 'Was able to claim with invalid proof', 'Invalid proof'); }); it(printTitle('node', 'can not claim same interval twice'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; // Create 3 snapshots await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); await submitRewards(2, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(2, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimRewards(registeredNode1.address, [0, 1], [rewards, rewards], { from: registeredNode1, }); await shouldRevert(claimRewards(registeredNode1.address, [0], [rewards], { from: registeredNode1, }), 'Was able to claim again', 'Already claimed'); await shouldRevert(claimRewards(registeredNode1.address, [1], [rewards], { from: registeredNode1, }), 'Was able to claim again', 'Already claimed'); await shouldRevert(claimRewards(registeredNode1.address, [0, 1], [rewards, rewards], { from: registeredNode1, }), 'Was able to claim again', 'Already claimed'); await shouldRevert(claimRewards(registeredNode1.address, [0, 2], [rewards, rewards], { from: registeredNode1, }), 'Was able to claim again', 'Already claimed'); }); it(printTitle('node', 'can claim mulitiple periods in a single tx'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, { address: registeredNode2.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '2'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; // Submit 2 snapshots await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); await submitRewards(2, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(2, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimRewards(registeredNode1.address, [0], [rewards], { from: registeredNode1, }); await claimRewards(registeredNode1.address, [1, 2], [rewards, rewards], { from: registeredNode1, }); await claimRewards(registeredNode2.address, [0, 1, 2], [rewards, rewards, rewards], { from: registeredNode2, }); }); it(printTitle('node', 'can claim RPL and stake'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, { address: registeredNode2.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '2'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimAndStakeRewards(registeredNode1.address, [0], [rewards], '1'.ether, { from: registeredNode1, }); await claimAndStakeRewards(registeredNode2.address, [0], [rewards], '2'.ether, { from: registeredNode2, }); // Do a second claim interval await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimAndStakeRewards(registeredNode1.address, [1], [rewards], '0.5'.ether, { from: registeredNode1, }); await claimAndStakeRewards(registeredNode2.address, [1], [rewards], '1'.ether, { from: registeredNode2, }); }); it(printTitle('node', 'can not claim RPL and stake from non-rpl-withdrawal credential address if RPL withdrawal credentials set'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Set RPL withdrawal address to a random address await setNodeRPLWithdrawalAddress(registeredNode1, random, { from: node1WithdrawalAddress }); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Can't claim from node address await shouldRevert(claimAndStakeRewards(registeredNode1.address, [0], [rewards], '1'.ether, { from: registeredNode1, }), 'Was able to claim', 'Can only claim and stake from RPL withdrawal address'); // Can't claim from withdrawal address await shouldRevert(claimAndStakeRewards(registeredNode1.address, [0], [rewards], '1'.ether, { from: node1WithdrawalAddress, }), 'Was able to claim', 'Can only claim and stake from RPL withdrawal address'); // Can claim from rpl withdrawal address await claimAndStakeRewards(registeredNode1.address, [0], [rewards], '1'.ether, { from: random, }); }); it(printTitle('node', 'can claim and stake RPL from withdrawal address if RPL withdrawal address not set'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Can claim from withdrawal address await claimAndStakeRewards(registeredNode1.address, [0], [rewards], '1'.ether, { from: node1WithdrawalAddress, }); }); it(printTitle('node', 'can not stake amount greater than claim'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await shouldRevert(claimAndStakeRewards(registeredNode1.address, [0], [rewards], '2'.ether, { from: registeredNode1, }), 'Was able to stake amount greater than reward', 'Invalid stake amount'); }); it(printTitle('node', 'can claim RPL and stake multiple snapshots'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(1, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim RPL await claimAndStakeRewards(registeredNode1.address, [0, 1], [rewards, rewards], '2'.ether, { from: registeredNode1, }); }); /*** Random *************************/ it(printTitle('random', 'can execute reward period if consensus is reached'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Add another 2 trusted nodes so consensus becomes 3 votes await setNodeTrusted(unregisteredNodeTrusted1, 'saas_3', 'node@home.com', owner); await setNodeTrusted(unregisteredNodeTrusted2, 'saas_4', 'node@home.com', owner); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Kick a trusted node so consensus becomes 2 votes again await kickTrustedNode(unregisteredNodeTrusted1, [registeredNodeTrusted1, registeredNodeTrusted2, unregisteredNodeTrusted1]); // Now we should be able to execute the reward period await executeRewards(0, rewards, '0'.ether, '0'.ether, { from: random }); }); it(printTitle('random', 'cant execute reward period twice'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Add another 2 trusted nodes so consensus becomes 3 votes await setNodeTrusted(unregisteredNodeTrusted1, 'saas_3', 'node@home.com', owner); await setNodeTrusted(unregisteredNodeTrusted2, 'saas_4', 'node@home.com', owner); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Kick a trusted node so consensus becomes 2 votes again await kickTrustedNode(unregisteredNodeTrusted1, [registeredNodeTrusted1, registeredNodeTrusted2, unregisteredNodeTrusted1]); // Now we should be able to execute the reward period await executeRewards(0, rewards, '0'.ether, '0'.ether, { from: random }); await shouldRevert(executeRewards(0, rewards, '0'.ether, '0'.ether, { from: random }), 'Already executed'); }); it(printTitle('random', 'can submit past consensus'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Add another trusted node await setNodeTrusted(unregisteredNodeTrusted1, 'saas_3', 'node@home.com', owner); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // already have consensus, should have executed await shouldRevert(executeRewards(0, rewards, '0'.ether, '0'.ether, { from: random }), 'Already executed'); // should allow another vote past consensus await submitRewards(0, rewards, '0'.ether, '0'.ether, '0'.ether, { from: unregisteredNodeTrusted1 }); }); /*** Misc *************************/ it(printTitle('misc', 'claim bitmap is correct'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '1'.ether, nodeETH: '0'.ether, voterETH: 0n }, ]; // Submit 10 reward intervals for (let i = 0; i < 10; i++) { await submitRewards(i, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(i, rewards, '0'.ether, '0'.ether, '0'.ether, { from: registeredNodeTrusted2 }); } // Some arbitrary intervals to claim let claimIntervals = [0, 4, 6, 9]; await claimRewards(registeredNode1.address, claimIntervals, Array(claimIntervals.length).fill(rewards), { from: registeredNode1, }); // Retrieve the bitmap of claims const rocketStorage = await RocketStorage.deployed(); const key = ethers.solidityPackedKeccak256( ['string', 'address', 'uint256'], ['rewards.interval.claimed', registeredNode1.address, 0n], ); const bitmap = Number(await rocketStorage.getUint(key)); // Construct the expected bitmap and compare let expected = 0; for (let i = 0; i < claimIntervals.length; i++) { expected |= 1 << claimIntervals[i]; } assert.strictEqual(bitmap, expected, 'Incorrect claimed bitmap'); // Confirm second claim fails for each interval for (let i = 0; i < claimIntervals.length; i++) { await shouldRevert(claimRewards(registeredNode1.address, [claimIntervals[i]], [rewards], { from: registeredNode1, }), 'Was able to claim again', 'Already claimed'); } }); it(printTitle('withdrawal address', 'can recover ETH rewards on reverting transfer to withdrawal address'), async () => { // Initialize RPL inflation & claims contract let rplInflationStartTime = await rplInflationSetup(); await rewardsContractSetup('0.5'.ether, '0'.ether, '0.5'.ether); // Set RPL withdrawal address to the revert on transfer helper const revertOnTransfer = await RevertOnTransfer.deployed(); await setNodeWithdrawalAddress(registeredNode1, revertOnTransfer.target, { from: node1WithdrawalAddress }); // Move to inflation start plus one claim interval let currentTime = BigInt(await helpers.time.latest()); assertBN.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time'); await helpers.time.increase(rplInflationStartTime - currentTime + claimIntervalTime); // Send ETH to rewards pool const rocketSmoothingPool = await RocketSmoothingPool.deployed(); await owner.sendTransaction({ to: rocketSmoothingPool.target, value: '20'.ether, }); // Submit rewards snapshot const rewards = [ { address: registeredNode1.address, network: 0, trustedNodeRPL: '0'.ether, nodeRPL: '0'.ether, nodeETH: '1'.ether, voterETH: 0n }, ]; await submitRewards(0, rewards, '0'.ether, '1'.ether, '0'.ether, { from: registeredNodeTrusted1 }); await submitRewards(0, rewards, '0'.ether, '1'.ether, '0'.ether, { from: registeredNodeTrusted2 }); // Claim from node which should fail to send the ETH and increase outstanding balance await claimAndStakeRewards(registeredNode1.address, [0], [rewards], '0'.ether, { from: registeredNode1, }); const rocketMerkleDistributorMainnet = await RocketMerkleDistributorMainnet.deployed(); // Check outstanding balance is correct const balance = await rocketMerkleDistributorMainnet.getOutstandingEth(revertOnTransfer.target); assertBN.equal(balance, '1'.ether); // Attempt to claim the ETH from the previously reverting withdrawal address await revertOnTransfer.setEnabled(false); const payload = rocketMerkleDistributorMainnet.interface.encodeFunctionData('claimOutstandingEth()'); await revertOnTransfer.call(rocketMerkleDistributorMainnet.target, payload); // Check ETH was sent to withdrawal address const withdrawalAddressBalance = await ethers.provider.getBalance(revertOnTransfer.target); assertBN.equal(withdrawalAddressBalance, '1'.ether); }); }); } ================================================ FILE: test/rewards/scenario-claim-and-stake-rewards.js ================================================ import { RocketMerkleDistributorMainnet, RocketNodeManager, RocketNodeStaking, RocketRewardsPool, RocketTokenRPL, } from '../_utils/artifacts'; import { parseRewardsMap } from '../_utils/merkle-tree'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit network prices export async function claimAndStakeRewards(nodeAddress, indices, rewards, stakeAmount, txOptions) { // Load contracts const [ rocketRewardsPool, rocketNodeManager, rocketNodeStaking, rocketMerkleDistributorMainnet, rocketTokenRPL, ] = await Promise.all([ RocketRewardsPool.deployed(), RocketNodeManager.deployed(), RocketNodeStaking.deployed(), RocketMerkleDistributorMainnet.deployed(), RocketTokenRPL.deployed(), ]); // Get node withdrawal address let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Get balances function getBalances() { return Promise.all([ rocketRewardsPool.getClaimIntervalTimeStart(), rocketTokenRPL.balanceOf(nodeWithdrawalAddress), rocketNodeStaking.getNodeStakedRPL(nodeAddress), ethers.provider.getBalance(nodeWithdrawalAddress), rocketMerkleDistributorMainnet.getOutstandingEth(nodeWithdrawalAddress), ]).then( ([claimIntervalTimeStart, nodeRpl, rplStake, nodeEth, outstandingEth]) => ({claimIntervalTimeStart, nodeRpl, rplStake, nodeEth, outstandingEth}) ); } let [balances1] = await Promise.all([ getBalances(), ]); // Construct claim arguments let claimer = nodeAddress; let totalAmountRPL = 0n; let totalAmountETH = 0n; let claims = [] for (let i = 0; i < indices.length; i++) { let treeData = parseRewardsMap(rewards[i]); let proof = treeData.proof.claims[ethers.getAddress(claimer)]; if (!proof) { throw new Error('No proof in merkle tree for ' + claimer) } claims.push({ rewardIndex: indices[i], amountRPL: proof.amountRPL, amountSmoothingPoolETH: proof.amountSmoothingPoolETH, amountVoterETH: proof.amountVoterETH, merkleProof: proof.proof }) totalAmountRPL = totalAmountRPL + proof.amountRPL; totalAmountETH = totalAmountETH + proof.amountSmoothingPoolETH + proof.amountVoterETH; } const tx = await rocketMerkleDistributorMainnet.connect(txOptions.from).claimAndStake(nodeAddress, claims, stakeAmount, txOptions); let gasUsed = 0n; if(nodeWithdrawalAddress.toLowerCase() === txOptions.from.address.toLowerCase()) { const txReceipt = await tx.wait(); gasUsed = BigInt(txReceipt.gasUsed * txReceipt.gasPrice); } let [balances2] = await Promise.all([ getBalances(), ]); let amountStaked = balances2.rplStake - balances1.rplStake; assertBN.equal(balances2.nodeRpl - balances1.nodeRpl, totalAmountRPL - amountStaked, 'Incorrect updated node RPL balance'); const ethDiff = balances2.nodeEth - balances1.nodeEth + gasUsed + balances2.outstandingEth - balances1.outstandingEth; assertBN.equal(ethDiff, totalAmountETH, 'Incorrect updated node ETH balance'); } ================================================ FILE: test/rewards/scenario-claim-rewards.js ================================================ import { RocketMerkleDistributorMainnet, RocketNodeManager, RocketRewardsPool, RocketTokenRPL, } from '../_utils/artifacts'; import { parseRewardsMap } from '../_utils/merkle-tree'; import { assertBN } from '../_helpers/bn'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit network prices export async function claimRewards(nodeAddress, indices, rewards, txOptions) { // Load contracts const [ rocketRewardsPool, rocketNodeManager, rocketMerkleDistributorMainnet, rocketTokenRPL, ] = await Promise.all([ RocketRewardsPool.deployed(), RocketNodeManager.deployed(), RocketMerkleDistributorMainnet.deployed(), RocketTokenRPL.deployed(), ]); // Get node withdrawal address let nodeWithdrawalAddress = await rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); let nodeRPLWithdrawalAddress = await rocketNodeManager.getNodeRPLWithdrawalAddress(nodeAddress); // Get balances function getBalances() { return Promise.all([ rocketRewardsPool.getClaimIntervalTimeStart(), rocketTokenRPL.balanceOf(nodeWithdrawalAddress), ethers.provider.getBalance(nodeWithdrawalAddress), ethers.provider.getBalance(nodeRPLWithdrawalAddress), ]).then( ([claimIntervalTimeStart, nodeRpl, nodeEth, nodeRplEth]) => ({ claimIntervalTimeStart, nodeRpl, nodeEth, nodeRplEth }), ); } let [balances1] = await Promise.all([ getBalances(), ]); // Construct claim arguments let claimer = nodeAddress; let totalAmountRPL = 0n; let totalAmountETH = 0n; let totalAmountVoterETH = 0n; let claims = [] for (let i = 0; i < indices.length; i++) { let treeData = parseRewardsMap(rewards[i]); let proof = treeData.proof.claims[ethers.getAddress(claimer)]; if (!proof) { throw new Error('No proof in merkle tree for ' + claimer) } claims.push({ rewardIndex: indices[i], amountRPL: proof.amountRPL, amountSmoothingPoolETH: proof.amountSmoothingPoolETH, amountVoterETH: proof.amountVoterETH, merkleProof: proof.proof }) totalAmountRPL = totalAmountRPL + proof.amountRPL; totalAmountETH = totalAmountETH + proof.amountSmoothingPoolETH; totalAmountVoterETH = totalAmountVoterETH + proof.amountVoterETH; } const tx = await rocketMerkleDistributorMainnet.connect(txOptions.from).claim(nodeAddress, claims, txOptions); let gasUsed = 0n; if (ethers.getAddress(nodeWithdrawalAddress) === ethers.getAddress(txOptions.from.address)) { const txReceipt = await tx.wait(); gasUsed = BigInt(txReceipt.gasUsed * txReceipt.gasPrice); } let [balances2] = await Promise.all([ getBalances(), ]); assertBN.equal(balances2.nodeRpl - balances1.nodeRpl, totalAmountRPL, 'Incorrect updated node RPL balance'); if (nodeRPLWithdrawalAddress === nodeWithdrawalAddress) { assertBN.equal(balances2.nodeEth - balances1.nodeEth + gasUsed, totalAmountETH + totalAmountVoterETH, 'Incorrect updated node ETH balance'); } else { assertBN.equal(balances2.nodeEth - balances1.nodeEth + gasUsed, totalAmountETH, 'Incorrect updated node ETH balance'); assertBN.equal(balances2.nodeRplEth - balances1.nodeRplEth, totalAmountVoterETH, 'Incorrect updated node voter ETH balance'); } } ================================================ FILE: test/rewards/scenario-rewards-claim.js ================================================ import { RocketDAOProtocolSettingsRewards, RocketRewardsPool } from '../../test/_utils/artifacts'; // Get the current rewards claim period in blocks export async function rewardsClaimIntervalTimeGet(txOptions) { // Load contracts const rocketDAOProtocolSettingsRewards = await RocketDAOProtocolSettingsRewards.deployed(); return await rocketDAOProtocolSettingsRewards.getClaimIntervalTime.call(); } // Get the current rewards claimers total export async function rewardsClaimersPercTotalGet(txOptions) { // Load contracts const rocketDAOProtocolSettingsRewards = await RocketDAOProtocolSettingsRewards.deployed(); return await rocketDAOProtocolSettingsRewards.getRewardsClaimersPercTotal.call(); } // Get how many seconds needed until the next claim interval export async function rewardsClaimIntervalsPassedGet(txOptions) { // Load contracts const rocketRewardsPool = await RocketRewardsPool.deployed(); return await rocketRewardsPool.getClaimIntervalsPassed.call(); } ================================================ FILE: test/rewards/scenario-submit-rewards.js ================================================ import { RocketClaimDAO, RocketDAONodeTrusted, RocketRewardsPool, RocketSmoothingPool, RocketTokenRETH, RocketTokenRPL, RocketVault, } from '../_utils/artifacts'; import { parseRewardsMap } from '../_utils/merkle-tree'; import { assertBN } from '../_helpers/bn'; import * as assert from 'assert'; const hre = require('hardhat'); const ethers = hre.ethers; // Submit rewards export async function submitRewards(index, rewards, treasuryRPL, userETH, treasuryETH, txOptions) { // Load contracts const [ rocketDAONodeTrusted, rocketRewardsPool, rocketTokenRETH, rocketTokenRPL, rocketClaimDAO, rocketVault, rocketSmoothingPool, ] = await Promise.all([ RocketDAONodeTrusted.deployed(), RocketRewardsPool.deployed(), RocketTokenRETH.deployed(), RocketTokenRPL.deployed(), RocketClaimDAO.deployed(), RocketVault.deployed(), RocketSmoothingPool.deployed(), ]); // Get parameters let trustedNodeCount = await rocketDAONodeTrusted.getMemberCount(); // Construct the merkle tree let treeData = parseRewardsMap(rewards); const trustedNodeRPL = []; const nodeRPL = []; const nodeETH = []; let maxNetwork = rewards.reduce((a, b) => Math.max(a, b.network), 0); for (let i = 0; i <= maxNetwork; i++) { trustedNodeRPL[i] = 0n; nodeRPL[i] = 0n; nodeETH[i] = 0n; } for (let i = 0; i < rewards.length; i++) { trustedNodeRPL[rewards[i].network] = trustedNodeRPL[rewards[i].network] + rewards[i].trustedNodeRPL; nodeRPL[rewards[i].network] = nodeRPL[rewards[i].network] + rewards[i].nodeRPL; nodeETH[rewards[i].network] = nodeETH[rewards[i].network] + rewards[i].nodeETH + rewards[i].voterETH; } const totalETHRequired = userETH + treasuryETH + nodeETH.reduce((a,b) => a + b, 0n); const smoothingPoolTotal = await ethers.provider.getBalance(rocketSmoothingPool.target) const rewardsPoolTotal = await rocketVault.balanceOf('rocketRewardsPool') if (totalETHRequired > smoothingPoolTotal + rewardsPoolTotal) { throw new Error('Not enough ETH in smoothing pool and rewards pool for rewards') } let smoothingPoolETH = 0 if (totalETHRequired > rewardsPoolTotal) { smoothingPoolETH = totalETHRequired - rewardsPoolTotal } const root = treeData.proof.merkleRoot; const submission = { rewardIndex: index, executionBlock: 0n, consensusBlock: 0n, merkleRoot: root, intervalsPassed: 1n, smoothingPoolETH, treasuryRPL: treasuryRPL, treasuryETH: treasuryETH, userETH: userETH, trustedNodeRPL: trustedNodeRPL, nodeRPL: nodeRPL, nodeETH: nodeETH, }; // Get submission details function getSubmissionDetails() { return Promise.all([ rocketRewardsPool.getTrustedNodeSubmitted(txOptions.from.address, index), rocketRewardsPool.getSubmissionCount(submission), ]).then( ([nodeSubmitted, count]) => ({ nodeSubmitted, count }), ); } async function getData() { let [submission, rewardIndex, treasuryRpl, treasuryEth, rethBalance, rewardsPoolBalance] = await Promise.all([ getSubmissionDetails(), rocketRewardsPool.getRewardIndex(), rocketTokenRPL.balanceOf(rocketClaimDAO.target), rocketVault.balanceOf('rocketClaimDAO'), ethers.provider.getBalance(rocketTokenRETH.target), ethers.provider.getBalance(rocketRewardsPool.target), ]); return {submission, rewardIndex, treasuryRpl, treasuryEth, rethBalance, rewardsPoolBalance}; } // Get initial submission details const data1 = await getData() let alreadyExecuted = submission.rewardIndex !== Number(data1.rewardIndex); // Submit prices await rocketRewardsPool.connect(txOptions.from).submitRewardSnapshot(submission, txOptions); const actualExecutionBlock = await ethers.provider.getBlockNumber(); assert.equal(await rocketRewardsPool.getSubmissionFromNodeExists(txOptions.from.address, submission), true); // Get updated submission details & prices const data2 = await getData() // Check if prices should be updated and were not updated yet let expectedExecute = (data2.submission.count * 2n) > trustedNodeCount && !alreadyExecuted; // Check submission details assert.equal(data1.submission.nodeSubmitted, false, 'Incorrect initial node submitted status'); assert.equal(data2.submission.nodeSubmitted, true, 'Incorrect updated node submitted status'); assertBN.equal(data2.submission.count, data1.submission.count + 1n, 'Incorrect updated submission count'); // Calculate changes in user ETH and treasury RPL let userETHChange = data2.rethBalance - data1.rethBalance; let treasuryRPLChange = data2.treasuryRpl - data1.treasuryRpl; let treasuryEthChange = data2.treasuryEth - data1.treasuryEth; // Check reward index and user balances if (expectedExecute) { assertBN.equal(data2.rewardIndex, data1.rewardIndex+ 1n, 'Incorrect updated network prices block'); assertBN.equal(userETHChange, userETH, 'User ETH balance not correct'); assertBN.equal(treasuryRPLChange, treasuryRPL, 'Treasury RPL balance not correct'); assertBN.equal(treasuryEthChange, treasuryETH, 'Treasury ETH balance not correct'); // Check block and address const executionBlock = await rocketRewardsPool.getClaimIntervalExecutionBlock(index); const executionAddress = await rocketRewardsPool.getClaimIntervalExecutionAddress(index); assert.equal(executionBlock, actualExecutionBlock); assert.equal(executionAddress, rocketRewardsPool.target); } else { assertBN.equal(data2.rewardIndex, data1.rewardIndex, 'Incorrect updated network prices block'); assertBN.equal(data1.rethBalance, data2.rethBalance, 'User ETH balance changed'); assertBN.equal(data1.treasuryRpl, data2.treasuryRpl, 'Treasury RPL balance changed'); assertBN.equal(data1.treasuryEth, data2.treasuryEth, 'Treasury ETH balance changed'); } // No left over ETH in the rewards pool assertBN.equal(data2.rewardsPoolBalance, 0n, 'ETH was left in the rewards pool'); } // Execute a reward period that already has consensus export async function executeRewards(index, rewards, treasuryRPL, userETH, txOptions) { // Load contracts const [ rocketRewardsPool, ] = await Promise.all([ RocketRewardsPool.deployed(), ]); // Construct the merkle tree let treeData = parseRewardsMap(rewards); const trustedNodeRPL = []; const nodeRPL = []; const nodeETH = []; let maxNetwork = rewards.reduce((a, b) => Math.max(a, b.network), 0); for (let i = 0; i <= maxNetwork; i++) { trustedNodeRPL[i] = 0n; nodeRPL[i] = 0n; nodeETH[i] = 0n; } for (let i = 0; i < rewards.length; i++) { trustedNodeRPL[rewards[i].network] = trustedNodeRPL[rewards[i].network] + rewards[i].trustedNodeRPL; nodeRPL[rewards[i].network] = nodeRPL[rewards[i].network] + rewards[i].nodeRPL; nodeETH[rewards[i].network] = nodeETH[rewards[i].network] + rewards[i].nodeETH; } // // web3 doesn't like an array of BigNumbers, have to convert to dec string // for (let i = 0; i <= maxNetwork; i++) { // trustedNodeRPL[i] = trustedNodeRPL[i].toString(); // nodeRPL[i] = nodeRPL[i].toString(); // nodeETH[i] = nodeETH[i].toString(); // } const root = treeData.proof.merkleRoot; const treasuryETH = '0'.ether const submission = { rewardIndex: index, executionBlock: 0n, consensusBlock: 0n, merkleRoot: root, intervalsPassed: 1n, smoothingPoolETH: userETH + treasuryETH + nodeETH.reduce((a,b) => a + b, 0n), treasuryRPL: treasuryRPL, treasuryETH: treasuryETH, userETH: userETH, trustedNodeRPL: trustedNodeRPL, nodeRPL: nodeRPL, nodeETH: nodeETH, }; // Submit prices let rewardIndex1 = await rocketRewardsPool.getRewardIndex(); await rocketRewardsPool.connect(txOptions.from).executeRewardSnapshot(submission, txOptions); let rewardIndex2 = await rocketRewardsPool.getRewardIndex(); // Check index incremented assertBN.equal(rewardIndex2, rewardIndex1 + 1n, 'Incorrect updated network prices block'); } ================================================ FILE: test/rocket-pool-tests.js ================================================ import { beforeEach, afterEach, before, after } from 'mocha'; import { endSnapShot, startSnapShot } from './_utils/snapshotting'; import { deployRocketPool } from './_helpers/deployment'; import { setDefaultParameters } from './_helpers/defaults'; import { injectBNHelpers } from './_helpers/bn'; import { checkInvariants } from './_helpers/invariants'; import auctionTests from './auction/auction-tests'; import daoProtocolTests from './dao/dao-protocol-tests'; import daoProtocolTreasuryTests from './dao/dao-protocol-treasury-tests'; import daoNodeTrustedTests from './dao/dao-node-trusted-tests'; import daoSecurityTests from './dao/dao-security-tests'; import depositPoolTests from './deposit/deposit-pool-tests'; import networkBalancesTests from './network/network-balances-tests'; import networkFeesTests from './network/network-fees-tests'; import networkPricesTests from './network/network-prices-tests'; import nodeManagerTests from './node/node-manager-tests'; import nodeStakingTests from './node/node-staking-tests'; import nodeDistributorTests from './node/node-distributor-tests'; import rethTests from './token/reth-tests'; import rplTests from './token/rpl-tests'; import rewardsPoolTests from './rewards/rewards-tests'; import megapoolTests from './megapool/megapool-tests'; import networkSnapshotsTests from './network/network-snapshots-tests'; import networkVotingTests from './network/network-voting-tests'; import networkRevenuesTests from './network/network-revenues-tests'; import utilTests from './util/util-tests'; import verifierTests from './util/verifier-tests'; // Header console.log('\n'); console.log('______ _ _ ______ _ '); console.log('| ___ \\ | | | | | ___ \\ | |'); console.log('| |_/ /___ ___| | _____| |_ | |_/ /__ ___ | |'); console.log('| // _ \\ / __| |/ / _ \\ __| | __/ _ \\ / _ \\| |'); console.log('| |\\ \\ (_) | (__| < __/ |_ | | | (_) | (_) | |'); console.log('\\_| \\_\\___/ \\___|_|\\_\\___|\\__| \\_| \\___/ \\___/|_|'); // BN helpers injectBNHelpers(); // State snapshotting and gas usage tracking beforeEach(startSnapShot); afterEach(checkInvariants); afterEach(endSnapShot); before(async function() { // Deploy Rocket Pool await deployRocketPool(); // Set starting parameters for all tests await setDefaultParameters(); }); // Run tests auctionTests(); daoProtocolTests(); daoProtocolTreasuryTests(); daoNodeTrustedTests(); daoSecurityTests(); depositPoolTests(); megapoolTests(); networkBalancesTests(); networkFeesTests(); networkPricesTests(); networkSnapshotsTests(); networkVotingTests(); networkRevenuesTests(); nodeManagerTests(); nodeStakingTests(); nodeDistributorTests(); rethTests(); rplTests(); rewardsPoolTests(); utilTests(); verifierTests(); ================================================ FILE: test/token/reth-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { getDepositExcessBalance, userDeposit } from '../_helpers/deposit'; import { registerNode, setNodeTrusted } from '../_helpers/node'; import { depositExcessCollateral, getRethBalance, getRethCollateralRate, getRethTotalSupply } from '../_helpers/tokens'; import { burnReth } from './scenario-reth-burn'; import { transferReth } from './scenario-reth-transfer'; import { RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsNetwork, RocketDepositPool, RocketTokenRETH, } from '../_utils/artifacts'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; import { getMegapoolForNode, nodeDeposit } from '../_helpers/megapool'; import { submitBalances } from '../_helpers/network'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketTokenRETH', () => { let owner, node, trustedNode, staker1, staker2, random; // Setup const submitPricesFrequency = 500; const depositFeePerc = '0.005'.ether; // 0.5% deposit fee const rethCollateralRate = '1'.ether; before(async () => { await globalSnapShot(); [ owner, node, trustedNode, staker1, staker2, random, ] = await ethers.getSigners(); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '0'.ether, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.submit.prices.frequency', submitPricesFrequency, { from: owner }); await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.fee', depositFeePerc, { from: owner }); }); it(printTitle('rETH holder', 'can transfer rETH after deposit'), async () => { // Make user deposit const depositAmount = '20'.ether; await userDeposit({ from: staker2, value: depositAmount }); const rethBalance = await getRethBalance(staker1.address); // Transfer rETH await transferReth(random, rethBalance, { from: staker1, }); }); it(printTitle('rETH holder', 'can transfer rETH if received via transfer'), async () => { // Make user deposit const depositAmount = '20'.ether; await userDeposit({ from: staker2, value: depositAmount }); const rethBalance = await getRethBalance(staker1.address); // Transfer rETH await transferReth(random, rethBalance, { from: staker1, }); // Transfer rETH again await transferReth(staker1, rethBalance, { from: random, }); }); it(printTitle('rETH holder', 'can burn rETH for ETH collateral'), async () => { // Make user deposit const depositAmount = '20'.ether; await userDeposit({ from: staker1, value: depositAmount }); const rethBalance = await getRethBalance(staker1.address); // Burn rETH await burnReth(rethBalance, { from: staker1, }); }); it(printTitle('rETH holder', 'cannot burn an invalid amount of rETH'), async () => { // Make user deposit const depositAmount = '20'.ether; await userDeposit({ from: staker1, value: depositAmount }); const rethBalance = await getRethBalance(staker1.address); // Get burn amounts let burnZero = '0'.ether; let burnExcess = '100'.ether; assertBN.isAbove(burnExcess, rethBalance, 'Burn amount does not exceed rETH balance'); // Attempt to burn 0 rETH await shouldRevert(burnReth(burnZero, { from: staker1, }), 'Burned an invalid amount of rETH'); // Attempt to burn too much rETH await shouldRevert(burnReth(burnExcess, { from: staker1, }), 'Burned an amount of rETH greater than the token balance'); }); it(printTitle('random', 'can deposit excess collateral into the deposit pool'), async () => { // Send enough ETH to rETH contract to exceed target collateralisation rate const rocketTokenRETH = await RocketTokenRETH.deployed(); await random.sendTransaction({ to: rocketTokenRETH.target, value: '32'.ether, }); // Call the deposit excess function await depositExcessCollateral({ from: random }); // Collateral should now be at the target rate const collateralRate = await getRethCollateralRate(); // Collateral rate should now be 1 (the target rate) assertBN.equal(collateralRate, '1'.ether); }); describe('With node deposit', () => { let rethBalance; const depositAmount = '28'.ether; before(async () => { // Make a user deposit enough to create a 4 ETH bonded validator await userDeposit({ from: staker1, value: depositAmount }); rethBalance = await getRethBalance(staker1.address); // Register trusted node await registerNode({ from: trustedNode }); await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); // Register node and create a validator await registerNode({ from: node }); await nodeDeposit(node); }); it(printTitle('rETH holder', 'can burn rETH for excess deposit pool ETH'), async () => { // Make a user deposit from another stake large enough to burn staker1's rETH balance await userDeposit({ from: staker2, value: depositAmount }); // Check deposit pool excess balance let excessBalance = await getDepositExcessBalance(); assertBN.equal(excessBalance, depositAmount, 'Incorrect deposit pool excess balance'); // Burn rETH await burnReth(rethBalance, { from: staker1, }); }); it(printTitle('rETH holder', 'deposit below target collateral funds rETH contract'), async () => { // Set target collateral rate to 10% await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '0.1'.ether, { from: owner }); // Submit balances so target collateral can be calculated let rethSupply = await getRethTotalSupply(); let slotTimestamp = '1600000000'; await submitBalances(1, slotTimestamp, '28'.ether, '28'.ether, rethSupply, { from: trustedNode }); // A deposit of 1 ETH should fund rETH with the full 1 ETH as it's below the 2.8 ETH target await userDeposit({ from: staker2, value: '1'.ether }); // Check result const rocketTokenRETH = await RocketTokenRETH.deployed(); const rETHBalance = await ethers.provider.getBalance(rocketTokenRETH.target); assertBN.equal(rETHBalance, '1'.ether); }); it(printTitle('rETH holder', 'deposit above target collateral funds deposit pool'), async () => { // Set target collateral rate to 10% await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.reth.collateral.target', '0.1'.ether, { from: owner }); // Submit balances so target collateral can be calculated let rethSupply = await getRethTotalSupply(); let slotTimestamp = '1600000000'; await submitBalances(1, slotTimestamp, '28'.ether, '28'.ether, rethSupply, { from: trustedNode }); // A deposit of 56 ETH should fund rETH with 2.8 ETH to hit target collateral and remainder to deposit pool await userDeposit({ from: staker2, value: '56'.ether }); // Check result const rocketTokenRETH = await RocketTokenRETH.deployed(); const rETHBalance = await ethers.provider.getBalance(rocketTokenRETH.target); const rocketDepositPool = await RocketDepositPool.deployed(); const depositPoolBalance = await rocketDepositPool.getBalance(); assertBN.equal(rETHBalance, '2.8'.ether); assertBN.equal(depositPoolBalance, '56'.ether - '2.8'.ether); }); it(printTitle('rETH holder', 'cannot burn rETH with insufficient collateral'), async () => { // Attempt to burn rETH for contract collateral await shouldRevert(burnReth(rethBalance, { from: staker1, }), 'Burned rETH with an insufficient contract ETH balance'); // Make user deposit const depositAmount = '10'.ether; await userDeposit({ from: staker2, value: depositAmount }); // Check deposit pool excess balance let excessBalance = await getDepositExcessBalance(); assertBN.equal(excessBalance, depositAmount, 'Incorrect deposit pool excess balance'); // Attempt to burn rETH for excess deposit pool ETH await shouldRevert(burnReth(rethBalance, { from: staker1, }), 'Burned rETH with an insufficient deposit pool excess ETH balance'); }); }); // TODO: Add tests for burning rETH and receiving validator rewards once megapool distributes are implemented }); } ================================================ FILE: test/token/rpl-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { mintDummyRPL } from './scenario-rpl-mint-fixed'; import { burnFixedRPL } from './scenario-rpl-burn-fixed'; import { allowDummyRPL } from './scenario-rpl-allow-fixed'; import { rplClaimInflation, rplSetInflationConfig } from './scenario-rpl-inflation'; import { setRPLInflationIntervalRate, setRPLInflationStartTime } from '../dao/scenario-dao-protocol-bootstrap'; import { RocketTokenRPL } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { globalSnapShot } from '../_utils/snapshotting'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('RocketTokenRPL', () => { let owner, userOne; // One day in seconds const ONE_DAY = 24 * 60 * 60; // Setup let userOneRPLBalance = '100'.ether; before(async () => { await globalSnapShot(); [ owner, userOne, ] = await ethers.getSigners(); // Mint RPL fixed supply for the users to simulate current users having RPL await mintDummyRPL(userOne, userOneRPLBalance, { from: owner }); }); it(printTitle('userOne', 'burn all their current fixed supply RPL for new RPL'), async () => { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // Give allowance for all to be sent await allowDummyRPL(rocketTokenRPL.target, userOneRPLBalance, { from: userOne, }); // Burn existing fixed supply RPL for new RPL await burnFixedRPL(userOneRPLBalance, { from: userOne, }); }); it(printTitle('userOne', 'burn less fixed supply RPL than they\'ve given an allowance for'), async () => { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // The allowance let allowance = userOneRPLBalance / 2n; // Give allowance for half to be spent await allowDummyRPL(rocketTokenRPL.target, allowance, { from: userOne, }); // Burn existing fixed supply RPL for new RPL await burnFixedRPL(allowance - '0.000001'.ether, { from: userOne, }); }); it(printTitle('userOne', 'fails to burn more fixed supply RPL than they\'ve given an allowance for'), async () => { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // The allowance let allowance = userOneRPLBalance - '0.000001'.ether; // Give allowance for all to be sent await allowDummyRPL(rocketTokenRPL.target, allowance, { from: userOne, }); // Burn existing fixed supply RPL for new RPL await shouldRevert(burnFixedRPL(userOneRPLBalance, { from: userOne, }), 'Burned more RPL than had gave allowance for'); }); it(printTitle('userOne', 'fails to burn more fixed supply RPL than they have'), async () => { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); // Give allowance for all to be sent await allowDummyRPL(rocketTokenRPL.target, userOneRPLBalance, { from: userOne, }); // Burn existing fixed supply RPL for new RPL await shouldRevert(burnFixedRPL(userOneRPLBalance + '0.000001'.ether, { from: userOne, }), 'Burned more RPL than had owned and had given allowance for'); }); it(printTitle('userOne', 'fails to set start time for inflation'), async () => { // Current time let currentTime = await helpers.time.latest(); // Set the start time for inflation await shouldRevert(setRPLInflationStartTime(currentTime + 3600, { from: userOne, }), 'Non owner set start time for inflation'); }); it(printTitle('guardian', 'succeeds setting future start time for inflation'), async () => { // Current time let currentTime = await helpers.time.latest(); // Set the start time for inflation await setRPLInflationStartTime(currentTime + 3600, { from: owner, }); }); it(printTitle('guardian', 'succeeds setting future start time for inflation twice'), async () => { // Current time let currentTime = await helpers.time.latest(); // Set the start time for inflation await setRPLInflationStartTime(currentTime + 3600, { from: owner, }); // Fast-forward await helpers.time.increase(1800); // Current time currentTime = await helpers.time.latest(); // Set the start time for inflation await setRPLInflationStartTime(currentTime + 3600, { from: owner, }); }); it(printTitle('guardian', 'fails to set start time for inflation less than current time'), async () => { // Current time let currentTime = await helpers.time.latest(); // Set the start block for inflation await shouldRevert(setRPLInflationStartTime(currentTime - 1800, { from: owner, }), 'Owner set old start block for inflation'); }); it(printTitle('guardian', 'fails to set start time for inflation after inflation has begun'), async () => { // Current time let currentTime = await helpers.time.latest(); // Inflation start time let inflationStartTime = currentTime + 3600; // Set the start time for inflation await setRPLInflationStartTime(inflationStartTime, { from: owner, }); // Fast-forward to when inflation has begun await helpers.time.increase(inflationStartTime + 60); // Current time currentTime = await helpers.time.latest(); // Set the start block for inflation await shouldRevert(setRPLInflationStartTime(currentTime + 3600, { from: owner, }), 'Owner set start block for inflation after it had started'); }); it(printTitle('userOne', 'fail to mint inflation before inflation start block has passed'), async () => { // Current time let currentTime = await helpers.time.latest(); let config = { timeStart: currentTime + 3600, timeClaim: currentTime + 1800, yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Run the test now const newTokens = await rplClaimInflation(config, { from: userOne }); assertBN.equal(newTokens, 0, 'Inflation claimed before start block has passed'); }); it(printTitle('userOne', 'fail to mint inflation before an interval has passed'), async () => { // Current time let currentTime = await helpers.time.latest(); let config = { timeStart: currentTime + 1800, timeClaim: currentTime + 3600, // Mid way through first interval yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Run the test now const newTokens = await rplClaimInflation(config, { from: userOne }); assertBN.equal(newTokens, 0, 'Inflation claimed before interval has passed'); }); it(printTitle('userOne', 'mint inflation midway through a second interval, then mint again after another interval'), async () => { // Current time let currentTime = await helpers.time.latest(); let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + (ONE_DAY * 2.5), // Claimm mid way through second interval yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Claim inflation half way through the second interval await rplClaimInflation(config, { from: userOne }); config.timeClaim += config.timeInterval; await rplClaimInflation(config, { from: userOne }); config.timeClaim += config.timeInterval; await rplClaimInflation(config, { from: userOne }); }); it(printTitle('userOne', 'mint inflation at multiple random intervals'), async () => { // Current time let currentTime = await helpers.time.latest(); const INTERVAL = ONE_DAY; const HALF_INTERVAL = INTERVAL / 2; let config = { timeInterval: INTERVAL, timeStart: currentTime + INTERVAL, timeClaim: currentTime + (INTERVAL * 5), yearlyInflationTarget: 0.025, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 3; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 10; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 20; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 24; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 32; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 38; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 53; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_INTERVAL * 70; await rplClaimInflation(config, { from: userOne }); }); it(printTitle('userOne', 'mint one years inflation after 365 days at 5% which would equal 18,900,000 tokens'), async () => { // Current time let currentTime = await helpers.time.latest(); const ONE_DAY = 24 * 60 * 60; let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + ONE_DAY + (ONE_DAY * 365), yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }, '18900000'); }); it(printTitle('userOne', 'mint one years inflation every quarter at 5% which would equal 18,900,000 tokens'), async () => { // Current time let currentTime = await helpers.time.latest(); const ONE_DAY = 24 * 60 * 60; const QUARTER_YEAR = ONE_DAY * 365 / 4; let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + ONE_DAY + QUARTER_YEAR, yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }); config.timeClaim += QUARTER_YEAR; await rplClaimInflation(config, { from: userOne }); config.timeClaim += QUARTER_YEAR; await rplClaimInflation(config, { from: userOne }); config.timeClaim += QUARTER_YEAR; await rplClaimInflation(config, { from: userOne }, '18900000'); }); it(printTitle('userTwo', 'mint two years inflation every 6 months at 5% which would equal 19,845,000 tokens'), async () => { // Current time let currentTime = await helpers.time.latest(); const ONE_DAY = 24 * 60 * 60; const HALF_YEAR = ONE_DAY * 365 / 2; let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + ONE_DAY + HALF_YEAR, yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_YEAR; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_YEAR; await rplClaimInflation(config, { from: userOne }); config.timeClaim += HALF_YEAR; await rplClaimInflation(config, { from: userOne }, '19845000'); }); it(printTitle('userOne', 'mint one years inflation, then set inflation rate to 0 to prevent new inflation'), async () => { // Current time let currentTime = await helpers.time.latest(); const ONE_DAY = 24 * 60 * 60; let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + ONE_DAY + (ONE_DAY * 365), yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }, '18900000'); // Now set inflation to 0 await setRPLInflationIntervalRate(0, { from: owner }); config.yearlyInflationTarget = 0; // Attempt to collect inflation config.timeClaim += (ONE_DAY * 365); const newTokens = await rplClaimInflation(config, { from: userOne }); assertBN.equal(newTokens, 0, 'Minted inflation after rate set to 0'); }); it(printTitle('userOne', 'mint one years inflation, then set inflation rate to 0 to prevent new inflation, then set inflation back to 5% for another year'), async () => { // Current time let currentTime = await helpers.time.latest(); const ONE_DAY = 24 * 60 * 60; let config = { timeInterval: ONE_DAY, timeStart: currentTime + ONE_DAY, timeClaim: currentTime + ONE_DAY + (ONE_DAY * 365), yearlyInflationTarget: 0.05, }; // Set config await rplSetInflationConfig(config, { from: owner }); // Mint inflation now await rplClaimInflation(config, { from: userOne }, '18900000'); // Now set inflation to 0 await setRPLInflationIntervalRate(0, { from: owner }); config.yearlyInflationTarget = 0; config.timeClaim += (ONE_DAY * 365); await rplClaimInflation(config, { from: userOne }, '18900000'); // Now set inflation back to 5% await setRPLInflationIntervalRate(0.05, { from: owner }); config.yearlyInflationTarget = 0.05; config.timeClaim += (ONE_DAY * 365); await rplClaimInflation(config, { from: userOne }, '19845000'); }); }); } ================================================ FILE: test/token/scenario-reth-burn.js ================================================ import { RocketTokenRETH } from '../../test/_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Burn rETH for ETH export async function burnReth(amount, txOptions) { // Load contracts const rocketTokenRETH = await RocketTokenRETH.deployed(); // Get balances function getBalances() { return Promise.all([ rocketTokenRETH.totalSupply(), rocketTokenRETH.balanceOf(txOptions.from), ethers.provider.getBalance(txOptions.from), ]).then( ([tokenSupply, userTokenBalance, userEthBalance]) => ({ tokenSupply, userTokenBalance, userEthBalance }), ); } // Get initial balances let balances1 = await getBalances(); // Set gas price let gasPrice = '20'.gwei; txOptions.gasPrice = gasPrice; // Burn tokens & get tx fee let tx = await rocketTokenRETH.connect(txOptions.from).burn(amount, txOptions); const txReceipt = await tx.wait(); let txFee = gasPrice * txReceipt.gasUsed; // Get updated balances let balances2 = await getBalances(); // Calculate values let burnAmount = amount; let expectedEthTransferred = await rocketTokenRETH.getEthValue(burnAmount); // Check balances assertBN.equal(balances2.tokenSupply, balances1.tokenSupply - burnAmount, 'Incorrect updated token supply'); assertBN.equal(balances2.userTokenBalance, balances1.userTokenBalance - burnAmount, 'Incorrect updated user token balance'); assertBN.equal(balances2.userEthBalance, balances1.userEthBalance + expectedEthTransferred - txFee, 'Incorrect updated user ETH balance'); } ================================================ FILE: test/token/scenario-reth-transfer.js ================================================ import { RocketTokenRETH } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Transfer rETH between accounts export async function transferReth(to, amount, txOptions) { // Load contracts const rocketTokenRETH = await RocketTokenRETH.deployed(); // Get balances function getBalances() { return Promise.all([ rocketTokenRETH.balanceOf(txOptions.from), rocketTokenRETH.balanceOf(to), ]).then( ([userFromTokenBalance, userToTokenBalance]) => ({ userFromTokenBalance, userToTokenBalance }), ); } // Get initial balances let balances1 = await getBalances(); // Transfer tokens await rocketTokenRETH.connect(txOptions.from).transfer(to, amount, txOptions); // Get updated balances let balances2 = await getBalances(); // Check balances assertBN.equal(balances2.userFromTokenBalance, balances1.userFromTokenBalance - amount, 'Incorrect updated user token balance'); assertBN.equal(balances2.userToTokenBalance, balances1.userToTokenBalance + amount, 'Incorrect updated user token balance'); } ================================================ FILE: test/token/scenario-rpl-allow-fixed.js ================================================ import { RocketTokenDummyRPL } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Allow RPL from the fixed contract to be spent export async function allowDummyRPL(to, amount, txOptions) { // Load contracts const rocketTokenDummyRPL = await RocketTokenDummyRPL.deployed(); // Get balances function getBalances() { return Promise.all([ rocketTokenDummyRPL.allowance(txOptions.from.address, to), ]).then( ([tokenAllowance]) => ({ tokenAllowance }), ); } // Get initial balances let balances1 = await getBalances(); // Mint tokens await rocketTokenDummyRPL.connect(txOptions.from).approve(to, amount, txOptions); // Get updated balances let balances2 = await getBalances(); // Calculate values let allowanceAmount = BigInt(amount); // Check balances assertBN.equal(balances2.tokenAllowance, balances1.tokenAllowance + allowanceAmount, 'Incorrect allowance for token'); } ================================================ FILE: test/token/scenario-rpl-burn-fixed.js ================================================ import { RocketTokenDummyRPL, RocketTokenRPL } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Burn current fixed supply RPL for new RPL export async function burnFixedRPL(amount, txOptions) { // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); const rocketTokenDummyRPL = await RocketTokenDummyRPL.deployed(); // Get balances function getBalances() { return Promise.all([ rocketTokenDummyRPL.balanceOf(txOptions.from), rocketTokenRPL.totalSupply(), rocketTokenRPL.balanceOf(txOptions.from), rocketTokenDummyRPL.balanceOf(rocketTokenRPL.target), rocketTokenRPL.balanceOf(rocketTokenRPL.target), ]).then( ([rplFixedUserBalance, rplTokenSupply, rplUserBalance, rplContractBalanceOfFixedSupply, rplContractBalanceOfSelf]) => ({ rplFixedUserBalance, rplTokenSupply, rplUserBalance, rplContractBalanceOfFixedSupply, rplContractBalanceOfSelf, }), ); } // Get initial balances let balances1 = await getBalances(); // Burn tokens & get tx fee await rocketTokenRPL.connect(txOptions.from).swapTokens(amount, txOptions); // Get updated balances let balances2 = await getBalances(); // Calculate values let mintAmount = BigInt(amount); // Check balances assertBN.equal(balances2.rplUserBalance, balances1.rplUserBalance + mintAmount, 'Incorrect updated user token balance'); assertBN.equal(balances2.rplContractBalanceOfSelf, balances1.rplContractBalanceOfSelf - mintAmount, 'RPL contract has not sent the RPL to the user address'); } ================================================ FILE: test/token/scenario-rpl-inflation.js ================================================ import { RocketTokenRPL, RocketVault } from '../_utils/artifacts'; import { setRPLInflationIntervalRate, setRPLInflationStartTime } from '../dao/scenario-dao-protocol-bootstrap'; import { assertBN } from '../_helpers/bn'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); // Set inflation config export async function rplSetInflationConfig(config, txOptions) { // Set the daily inflation start block await setRPLInflationStartTime(config.timeStart, txOptions); // Set the daily inflation rate await setRPLInflationIntervalRate(config.yearlyInflationTarget, txOptions); } // Claim the inflation after a set amount of blocks have passed export async function rplClaimInflation(config, txOptions, tokenAmountToMatch = null) { // Convert param to BN if (tokenAmountToMatch) { tokenAmountToMatch = BigInt(tokenAmountToMatch); } // Load contracts const rocketTokenRPL = await RocketTokenRPL.deployed(); const rocketVault = await RocketVault.deployed(); // Get the previously last inflation calculated block const timeIntervalLastCalc = await rocketTokenRPL.getInflationCalcTime(); // Get data about the current inflation function getInflationData() { return Promise.all([ helpers.time.latest(), rocketTokenRPL.totalSupply(), rocketTokenRPL.getInflationIntervalStartTime(), rocketTokenRPL.getInflationIntervalsPassed(), rocketTokenRPL.getInflationIntervalRate(), rocketTokenRPL.getInflationCalcTime(), rocketTokenRPL.getInflationIntervalTime(), rocketTokenRPL.balanceOf(rocketVault.target), rocketVault.balanceOfToken('rocketRewardsPool', rocketTokenRPL.target), ]).then( ([currentTime, tokenTotalSupply, inflationStartTime, inflationIntervalsPassed, inflationIntervalRate, inflationCalcTime, intervalTime, rocketVaultBalanceRPL, rocketVaultInternalBalanceRPL]) => ({ currentTime, tokenTotalSupply, inflationStartTime, inflationIntervalsPassed, inflationIntervalRate, inflationCalcTime, intervalTime, rocketVaultBalanceRPL, rocketVaultInternalBalanceRPL, }), ); } // Get the current time so we can calculate how much time to pass to make it to the claim time let currentTime = await helpers.time.latest(); // Blocks to process as passing let timeToSimulatePassing = config.timeClaim - currentTime; // Simulate time passing await helpers.time.increase(timeToSimulatePassing); // Get initial data let inflationData1 = await getInflationData(); // Starting amount of total supply let totalSupplyStart = inflationData1.tokenTotalSupply; // Some expected data results based on the passed parameters let dailyInflation = ((1 + config.yearlyInflationTarget) ** (1 / (365))).toFixed(18).ether; let expectedInflationIntervalsPassed = Number(inflationData1.inflationIntervalsPassed); // How many tokens to be expected minted let expectedTokensMinted = '0'.ether; // Are we expecting inflation? have any intervals passed? if (inflationData1.inflationIntervalsPassed > 0) { // How much inflation to use based on intervals passed let newTotalSupply = totalSupplyStart; // Add an extra interval to the calculations match up for (let i = 0; i < expectedInflationIntervalsPassed; i++) { newTotalSupply = newTotalSupply * dailyInflation / BigInt(1e18); } // Calculate expected inflation amount expectedTokensMinted = newTotalSupply - totalSupplyStart; } // Claim tokens now await rocketTokenRPL.connect(txOptions.from).inflationMintTokens(txOptions); // Get inflation data let inflationData2 = await getInflationData(); // Ending amount of total supply let totalSupplyEnd = inflationData2.tokenTotalSupply; // Verify the minted amount is correct based on inflation rate etc assertBN.equal(expectedTokensMinted, totalSupplyEnd - totalSupplyStart, 'Incorrect amount of minted tokens expected'); // Verify the minted tokens are now stored in Rocket Vault on behalf of Rocket Rewards Pool assertBN.equal(inflationData2.rocketVaultInternalBalanceRPL, inflationData2.rocketVaultBalanceRPL, 'Incorrect amount of tokens stored in Rocket Vault for Rocket Rewards Pool'); // Are we verifying an exact amount of tokens given as a required parameter on this pass? if (tokenAmountToMatch) { tokenAmountToMatch = BigInt(tokenAmountToMatch); assertBN.equal(tokenAmountToMatch, totalSupplyEnd / '1'.ether, 'Given token amount does not match total supply made'); } return totalSupplyEnd - totalSupplyStart; } ================================================ FILE: test/token/scenario-rpl-mint-fixed.js ================================================ import { RocketTokenDummyRPL } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; // Mint RPL from the dummy RPL contract to simulate a user having existing fixed supply RPL export async function mintDummyRPL(to, amount, txOptions) { // Load contracts const rocketTokenDummyRPL = await RocketTokenDummyRPL.deployed(); // Get balances function getBalances() { return Promise.all([ rocketTokenDummyRPL.totalSupply(), rocketTokenDummyRPL.balanceOf(to), ]).then( ([tokenSupply, userTokenBalance]) => ({ tokenSupply, userTokenBalance }), ); } // Get initial balances let balances1 = await getBalances(); // Mint tokens await rocketTokenDummyRPL.connect(txOptions.from).mint(to, amount, txOptions); // Get updated balances let balances2 = await getBalances(); // Calculate values let mintAmount = BigInt(amount); // Check balances assertBN.equal(balances2.tokenSupply, balances1.tokenSupply + mintAmount, 'Incorrect updated token supply'); assertBN.equal(balances2.userTokenBalance, balances1.userTokenBalance + mintAmount, 'Incorrect updated user token balance'); } ================================================ FILE: test/util/util-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { shouldRevert } from '../_utils/testing'; import { LinkedListStorage } from '../_utils/artifacts'; import { globalSnapShot } from '../_utils/snapshotting'; import * as assert from 'node:assert'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('LinkedListStorage', () => { let random; const regularQueue = ethers.solidityPackedKeccak256(['string'], ['regular']) const expressQueue = ethers.solidityPackedKeccak256(['string'], ['express']) // Setup before(async () => { await globalSnapShot(); [ random, ] = await ethers.getSigners(); }); it(printTitle('random', 'pack/unpack shouldnt change values'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let item = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } let packedItem = await linkedListStorage.packItem(item) let unpackedItem = await linkedListStorage.unpackItem(packedItem) assert.equal(item.receiver, unpackedItem.receiver) assert.equal(item.validatorId, unpackedItem.validatorId) assert.equal(item.suppliedValue, unpackedItem.suppliedValue) assert.equal(item.requestedValue, unpackedItem.requestedValue) }); it(printTitle('random', 'can enqueue/dequeue items'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let itemIn = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } // enqueue 3 items, check for the correct indexOf and length await linkedListStorage.enqueueItem(regularQueue, itemIn); let indexOfFirst = await linkedListStorage.getIndexOf(regularQueue, itemIn) assert.equal(indexOfFirst, 1) let listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 1) itemIn.validatorId = 2 await linkedListStorage.enqueueItem(regularQueue, itemIn) listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 2) itemIn.validatorId = 3 await linkedListStorage.enqueueItem(regularQueue, itemIn) listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 3) itemIn.validatorId = 2 // remove the second item await linkedListStorage.removeItem(regularQueue, itemIn) let first = await linkedListStorage.getItem(regularQueue, 1); assert.equal(first.validatorId, 1) let last = await linkedListStorage.getItem(regularQueue, 3); assert.equal(last.validatorId, 3) await linkedListStorage.dequeueItem(regularQueue) listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 1) await linkedListStorage.dequeueItem(regularQueue) listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 0) }); it(printTitle('random', 'can remove the only queue item'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let itemIn = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } await linkedListStorage.enqueueItem(regularQueue, itemIn) await linkedListStorage.dequeueItem(regularQueue) let listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 0) }); it(printTitle('random', 'cannot add the same item twice'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let itemIn = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } await linkedListStorage.enqueueItem(regularQueue, itemIn) let listLength = await linkedListStorage.getLength(regularQueue); assert.equal(listLength, 1) await shouldRevert(linkedListStorage.enqueueItem(regularQueue, itemIn)) }); it(printTitle('random', 'indexOf for non existing item returns 0'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let itemIn = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } let indexOf = await linkedListStorage.getIndexOf(regularQueue, itemIn); assert.equal(indexOf, 0) }); it(printTitle('random', 'reverts when trying to remove non existent item'), async () => { const linkedListStorage = await LinkedListStorage.deployed(); let itemIn = { receiver: random.address, validatorId: 1, suppliedValue: 8000, requestedValue: 32000, } await linkedListStorage.enqueueItem(regularQueue, itemIn) itemIn.validatorId = 2 await shouldRevert(linkedListStorage.removeItem(regularQueue, itemIn)); }); }); } ================================================ FILE: test/util/verifier-tests.js ================================================ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; import { artifacts, BeaconStateVerifier, BlockRootsMock } from '../_utils/artifacts'; import * as assert from 'assert'; import { shouldRevert } from '../_utils/testing'; import { time } from '@nomicfoundation/hardhat-network-helpers'; import { globalSnapShot } from '../_utils/snapshotting'; const hre = require('hardhat'); const ethers = hre.ethers; export default function() { describe('BeaconStateVerifier', () => { let owner, node, random; const farFutureEpoch = '18446744073709551615'.BN; // Setup before(async () => { await globalSnapShot(); [ owner, node, random, ] = await ethers.getSigners(); }); it(printTitle('BeaconStateVerifier', 'Can verify slot with state proof'), async () => { const beaconStateVerifier = await BeaconStateVerifier.deployed(); const witnesses = [ "0x107700ea94f26790066a7b5d248efdb9ead6d3a8265d69aa9a7466104a8359d2", "0x96a9cb37455ee3201aed37c6bd0598f07984571e5f0593c99941cb50af942cb1", "0xfb9369c355197b96acb9ac274ac94f6312078687edb1538fe8f0f718e55f8d22", "0x2f5e4432933c270f8c6d55b0e5bda1f771a8e6ffbcc222469eed4aa8e548d7a7", "0x4a1cdba46459907ad2e90e7781f1d6073cf605c7606449342b50a8eb9e5b137a", "0xfd0a4ea0112343eba60ae9a15bef34084e4df95fb5d34166a722f94edde023d2", "0xfcfc159f32c11dda7e315ff5d981cb1a247e4d26b3c2dc0f2aa3b842c5262a4f", "0xed688fdbfba04ce68e541cd09db8ea609fd951dd06b7dc171f337dcfb4e7774c", "0xd3b4850ac5f8ec9a4cc48295f972656a9b2ba8d35e665cd53c51a8bb448f9a63" ]; const blockRoot = '0x26e397dd184ab83558a241a65847bf02406e26835b5a186fb0a2e05690958ad2'; const slot = 11821055n; const slotTimestamp = (slot * 12n + 1606824023n) + 12n; await beaconStateVerifier.setBlockRoot(slotTimestamp, blockRoot); const correctProof = { slot: slot, witnesses: witnesses, } assert.equal(await beaconStateVerifier.verifySlot(slotTimestamp, correctProof), true); }); it(printTitle('BeaconStateVerifier', 'Can verify validator with state proof'), async () => { const beaconStateVerifier = await BeaconStateVerifier.deployed(); const witnesses = [ '0xdac075cf29676e5da6a30cb2c2fab90b2661e0b921c599b384399380ae9ab5ab', '0x0e79aad03eedbd72563db50550bb4066abe422407b4f6edddce9ae47ae87e15b', '0xebdf30ee31c84aea37daf85c65c905719264e88f506188080d60ee4beb0a6bca', '0x5a768ecc1dceded186826a1ca61d3959ae00465f36040b5ab02d6385dd6ee3fa', '0xf845231c6cca9c053e7830ad67927bd1a7364f3ee01b209456b24db1cc0fa98f', '0xc22694f4ffe34e813350e4fa3fedeeba4ad118860b84d45447701472606ec9d1', '0xdeb26ad6b414dd628c2b3392ebe5ba3d41fc7104e9b563fc154ea002a5a95e31', '0x2c22e317222b49318ab555c08b5e1d0e7e226515fe5e4797887c9b3a609700be', '0x9f80e99e8d1ba1b236466a03f3f59015dfabe3b6d35e566993dcc959e5824ee3', '0x73e279452b714583a3bf38d7c37e7ea275e28cd09abe60b11f941a12e64f4241', '0x28394ea21c79ad3b29d7fbb8b252d96150b211b60900517b012eed8afdb92a3f', '0xd7ec1342a5f06b9f60fdc95761c1dbdf01f31f5a28d326652dc2d9f879c297c8', '0x3823b57415d99bba0a6089026e692c2e9817c750e42e2517511ef5cf29070d05', '0x6a7886b5f8716f0e12f359d9025b51d764130c235b1115c9faee188cecc99df6', '0x3f7ec07faa352ae9d48eb0d48d65dc952eedf590bac5cff6484ae43504170716', '0x7be17af70e96360a0cde54b7eac5e0850e3a55f13995494006b052fda415fd9c', '0x50cb128a7ee63948dc8a8959609f24e19cc5d4dd48ba1d02bbb5fd61ed938dad', '0xff99326be45cd5416caf569a7562acaaf63a74d40b66d2f33a0738846e3366a5', '0x2fe03ec77e9a4653a29520320269fd0e44622efd0bd5c114a84b5ad3d476341f', '0xfb40cfe24c512cf2df420be3204f1c60e882d27f3ea63280c4b7c03c691a9a7f', '0xee50bfe01d2ea4bdf323be746769cc44afff7457aea078483382c5c33ddc4230', '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', '0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0', '0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544', '0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765', '0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4', '0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1', '0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636', '0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c', '0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7', '0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff', '0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5', '0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d', '0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c', '0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327', '0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74', '0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76', '0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f', '0xaf911d0000000000000000000000000000000000000000000000000000000000', '0xc6341f0000000000000000000000000000000000000000000000000000000000', '0x5aaa91f9944dda3f57d531e52f4127e134092e1e57350e5980dd654eee511488', '0xf99a074fcb6bb2a5b79601d206ab4700e897f0515626aaf30957aea3271b47b9', '0xdb4fe5420f82e43be50d01801979113267495914c66b45e9dff78c2ce393d27e', '0x4a1cdba46459907ad2e90e7781f1d6073cf605c7606449342b50a8eb9e5b137a', '0xfd0a4ea0112343eba60ae9a15bef34084e4df95fb5d34166a722f94edde023d2', '0xfcfc159f32c11dda7e315ff5d981cb1a247e4d26b3c2dc0f2aa3b842c5262a4f', '0xed688fdbfba04ce68e541cd09db8ea609fd951dd06b7dc171f337dcfb4e7774c', '0xd3b4850ac5f8ec9a4cc48295f972656a9b2ba8d35e665cd53c51a8bb448f9a63', ]; const blockRoot = '0x26e397dd184ab83558a241a65847bf02406e26835b5a186fb0a2e05690958ad2'; const slot = 11821055n; const slotTimestamp = (slot * 12n + 1606824023n) + 12n; await beaconStateVerifier.setBlockRoot(slotTimestamp, blockRoot); const tooOldSlot = 100000n; const tooOldSlotTimestamp = (tooOldSlot * 12n + 1606824023n) + 12n; const correctProof = { validatorIndex: 1060378, validator: { pubkey: '0xb6544b67c27a9d9f460bd839b1a42d4edf4fedd2567a631ffe473f047acd539257dd326e5c969a08a5ae07db6fd8616c', withdrawalCredentials: '0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f', effectiveBalance: 32000000000n, slashed: false, activationEligibilityEpoch: 246886n, activationEpoch: 247130n, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: witnesses, }; const incorrectProof = { validatorIndex: 1060378, validator: { pubkey: '0xb6544b67c27a9d9f460bd839b1a42d4edf4fedd2567a631ffe473f047acd539257dd326e5c969a08a5ae07db6fd8616c', withdrawalCredentials: '0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f', effectiveBalance: 32000000000n, slashed: false, activationEligibilityEpoch: 246886n, activationEpoch: 247130n, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: [ '0x0000000000000000000000000000000000000000000000000000000000000000', ...witnesses.slice(1), ], }; const invalidWitnessLengthProof = { validatorIndex: 1060378, validator: { pubkey: '0xb6544b67c27a9d9f460bd839b1a42d4edf4fedd2567a631ffe473f047acd539257dd326e5c969a08a5ae07db6fd8616c', withdrawalCredentials: '0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f', effectiveBalance: 32000000000n, slashed: false, activationEligibilityEpoch: 246886n, activationEpoch: 247130n, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: [ ...witnesses.slice(1), ], }; const invalidCredentialsProof = { validatorIndex: 1060378, validator: { pubkey: '0xb6544b67c27a9d9f460bd839b1a42d4edf4fedd2567a631ffe473f047acd539257dd326e5c969a08a5ae07db6fd8616c', withdrawalCredentials: '0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293e', effectiveBalance: 32000000000n, slashed: false, activationEligibilityEpoch: 246886n, activationEpoch: 247130n, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: witnesses, }; const tooOldProof = { validatorIndex: 1060378, validator: { pubkey: '0xb6544b67c27a9d9f460bd839b1a42d4edf4fedd2567a631ffe473f047acd539257dd326e5c969a08a5ae07db6fd8616c', withdrawalCredentials: '0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f', effectiveBalance: 32000000000n, slashed: false, activationEligibilityEpoch: 246886n, activationEpoch: 247130n, exitEpoch: farFutureEpoch, withdrawableEpoch: farFutureEpoch, }, witnesses: witnesses, }; await shouldRevert( beaconStateVerifier.verifyValidator(tooOldSlotTimestamp, tooOldSlot, tooOldProof), 'Accepted pre-electra proof', 'Invalid proof', ); await shouldRevert( beaconStateVerifier.verifyValidator(slotTimestamp, slot, invalidWitnessLengthProof), 'Accepted invalid witness length', 'Invalid witness length', ); assert.equal(await beaconStateVerifier.verifyValidator(slotTimestamp, slot, incorrectProof), false); assert.equal(await beaconStateVerifier.verifyValidator(slotTimestamp, slot, invalidCredentialsProof), false); assert.equal(await beaconStateVerifier.verifyValidator(slotTimestamp, slot, correctProof), true); }); it(printTitle('BeaconStateVerifier', 'Can verify withdrawal with state proof'), async () => { const beaconStateVerifier = await BeaconStateVerifier.deployed(); const witnesses = [ '0x56ebcae55f5161bd71226301b4c751ef433864c820c8d09361ca1a74758dd72c', '0x162cc35aa31a7cf1790ca34860c7e7a63f1ab2529f66d99fa1a872aea0bcf529', '0x60df1c1e8c19fa3de668e029902080838b9dd7ab7fec07697512023010f94d8e', '0x2096b4b750bdd7e22648c95e859efbf140e3da2ddd9ccdd95f3eba97d8fc121b', '0x1000000000000000000000000000000000000000000000000000000000000000', '0x0000080000000000000000000000000000000000000000000000000000000000', '0x61f017d3d8dee5ba8c68636e51a096ed3f523bbf29209fdb88711ff91a013c00', '0x268dfefa9d9326b73496fceb1f0a5ef42c8186889d0e3afd04c96ea87438120c', '0xd85a7f6d61f27841b359f9d59db3adddd1208a0ea924ffbc9c229220f5a23c5a', '0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c', '0xfd243838556ef257a4f3fd56272677a294c981de157694a3908dc9c08ca75d7a', '0xbd44a705c5063628996d4655f67571bcb9feadccab563f32235b08f8d52e9c7d', '0x6dd3b9955d892d92338b19976fd07084bfe88a76c3063482b7f30ee60feb2a58', '0x0a08a05a0b40226edaf0b2f1283eef98aca4b4cbe11e5a5add681fb78a15e807', '0x0000000000000000000000000000000000000000000000000000000000000000', '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', '0xa5f81459647ffebe8131ca4450ab282041ee9392788322920d6c6453e0d3703b', '0x7fa5e2df1bc7aa2f1530cd0bf1d3eab30ab12c4c1759429be374f5ed5bbbe43f', '0x409b0b10e9827ef913ad8961fc41b5dd5a01958c74e216fcde0d41e4738cd35c', '0xde3665f1b9e597580bb07de60cea574c5979a7e004d3b32e374a6350cce8fac5', '0xe7be85faefef9065463ec965fc39cfca593eb821ab779ea554f75524c9f60a5a', '0x9aab9f93f2e6677e57dd7050f31c9f98a35b8a43baca479a7bcb19c2eda73dee', '0x9e81de4708be5491b91ac6063e8ddd6fdd337ee4e0a0cba3d514645f096455c8', '0x66e126f270d3a2e25f45a07f376e99d8d337294ae07881402b4559f6ab4aa196', '0x1a49505aca09512fe47c707f8e904b230c91f691ec5e740d56b4f114897f41d3', '0x7bf09154ce4ecb3b37e79aab07747b10daebf5beef9ea8bf21dabf19c1882ef4', '0xc66785fa60eea935ffd8f68b04add1c62ef58e38e981a9531a7dd0278efcd26b', '0xf3c3f22873777b958507460c3537f0bd918c418e5addf3a03430cff26ee07d9e', '0x687f364236011235b2b4e40731cca0162739608af0294a6d4b576b5aecf51e57', '0x4fbd98fec8d190f77e452402b07aa8bf65847a2e2377f2e37065be5a9fa265e9', '0xd24aa06a0c8898472fa28ae9b982d7e392936e02d8cb53c48197e2edf3ba9ad5', '0xbef846d51e9bc2f7d07e91d4ef8c723e086a7df6e046fcea4bd477628c19e8a7', '0x542dea61bd1defaed4819d65ead85d75d6d940a3d2dded3749e725536acd8a4b', '0x3e177dc62135a265fafb6040763bf023d30f7540828b152d3473604a9e887eef', '0xa61ec140c4dee2bec895af537f2ef19376fac4ddb292ad7352809d7d5926461e', '0x2395c64f50239f14feea5dbe13c65d405e0d4be2d25e8995d8f64b94f83014fc', '0x032ffdac4b987092a708a481a6aa53c66aa874fe96a9f689031715ffa726fee4', '0xbe6d4ac575061b5182c9112451fdc189c2d3dc3a882b4c06c365e0acded0d600', '0x6cb1b243918374de6252a32aa24a34e0e40f3df71b7b51e6a59d0e99e9109d2d', ]; const blockRoot = '0xe39be859f0aaa98d1c269252388115284366451b58ed082801593dbbfccd1876'; const slot = 11834166n; const slotTimestamp = (slot * 12n + 1606824023n) + 12n; await beaconStateVerifier.setBlockRoot(slotTimestamp, blockRoot); const tooOldSlot = 100000n; const tooOldSlotTimestamp = (tooOldSlot * 12n + 1606824023n) + 12n; const correctProof = { withdrawalSlot: 11825974n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: witnesses, }; const invalidProof = { withdrawalSlot: 11825974n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: [ '0x0000000000000000000000000000000000000000000000000000000000000000', ...witnesses.slice(1), ], }; const invalidWitnessLengthProof = { withdrawalSlot: 11825974n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: [ '0x0000000000000000000000000000000000000000000000000000000000000000', ], }; const incorrectAmountProof = { withdrawalSlot: 11825974n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165415n, }, witnesses: witnesses, }; const tooOldProof = { withdrawalSlot: 11825974n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: witnesses, }; const tooNewProof = { withdrawalSlot: slot, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: witnesses, }; const tooOldWithdrawalProof = { withdrawalSlot: 1000000n, withdrawalNum: 0n, withdrawal: { index: 89138507n, validatorIndex: 1060378, withdrawalCredentials: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amountInGwei: 19165416n, }, witnesses: witnesses, }; await shouldRevert( beaconStateVerifier.verifyWithdrawal(tooOldSlotTimestamp, tooOldSlot, tooOldProof), 'Accepted pre-electra proof', 'Invalid proof', ); await shouldRevert( beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, tooOldWithdrawalProof), 'Accepted pre-electra proof', 'Invalid proof', ); await shouldRevert( beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, tooNewProof), 'Accepted too recent proof', 'Invalid slot for proof', ); await shouldRevert( beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, invalidWitnessLengthProof), 'Accepted invalid witness length', 'Invalid witness length', ); assert.equal(await beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, invalidProof), false); assert.equal(await beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, incorrectAmountProof), false); assert.equal(await beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, correctProof), true); }); it(printTitle('BeaconStateVerifier', 'Can verify historical withdrawal with state proof'), async () => { const beaconStateVerifier = await BeaconStateVerifier.deployed(); const witnesses = [ '0x74cfc71c3b83d9ebf5efd08392c92a9dda42503dcad6803c73891d9053a70320', '0x93c4b29ead59124360480d4caa9654a5b0fdd65db4ea7a86b16e4b7b83bda95e', '0xe7fe50fcdea47e2a2f5a3fc124aa511e3509f7a15ba069b1ad498bbbe95b720d', '0x5deacc1ef4e1ce7209c023fec1ef703b2614c01be007c9d840ae16d5fd4a02db', '0x1000000000000000000000000000000000000000000000000000000000000000', '0x00000c0000000000000000000000000000000000000000000000000000000000', '0x468ac3f202e83aa85fc823833d1dd7e4c068247229ca20c93e03e09eec71b1f2', '0xdb4a3d44ad1639a5a33df330dacb43b1e9ffae933b39777284cc267cfb3b5e23', '0x554d7982fbf2b551698286f263c15bac3dab59aec4dad9ab151d65f87d2cebe3', '0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c', '0x78b61adb7dabe11b361c00c4d0ce8bc65ba5b25e986d53dd6c5f384c61407893', '0x969cccd23584b6103d59d51cce0c05c509f3c1c6388dee057aa797464fc156c2', '0x6dd3b9955d892d92338b19976fd07084bfe88a76c3063482b7f30ee60feb2a58', '0xade691acdbbfaad0986c3207cace76269ccfcbb43a7e7235e5c73034d922ce7b', '0x0000000000000000000000000000000000000000000000000000000000000000', '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', '0x6f09091e8b0c43ba767483032e44b9f7d188b5ccde3934bc34a13f25025a44fb', '0xdeb1cb883675e814da9c601b922023255ce1ada869d9094b29c63e4ac96cc439', '0x1a9b340cae67f6d4e0a710df062a1d60c35952905a1159a3e900a854cebb0cd3', '0x65df01553a531d51917456384133ddf678c6c889ce6162de9ea7dbe835564823', '0x9bdf14859c294df8627ca673abe55e5801b721ce4badb277e234b036439cc8a2', '0x9f980cdfbfaed8bd6dbc14b0e58eea9a78188bee1841289e61b2cb2b9b22e135', '0x04a9a49769c78c902f06bac0f0f4ab1d20a6f6a963e17d09f8bc8967a8a533c2', '0x9845c6e05b93d780abec431be46ed8693b206d12f826236710a0ab204c15d407', '0x223fbfd99ba532434d7a901e9571a02ce9abbe7ff8605e098f0377835126298b', '0xaf26da67d02d6cff9fb740177f937f8aa9254f72b1113e22dfe01b7bc32ef2e9', '0xd3163825b7a6359405907912a8deb6bf9367ce842fe7e84e43d87f331779f816', '0x64f656b7890973e7d6f1a52d2a9965662c6e4d7a70da5b8ea4a719bfbe7221a9', '0xf4e1a264be26a17650d3135f871c1fa481bdd5b0aec7e23385ea945e32357a88', '0x7c5fe548aa993a78739b599b58480c5e12cdf4ca1ece180d2f98af468bf4ee8b', '0x678bc097998c1ab127329d2416aa2149f9d61f3e174813f2fcc1c5f3f82dfbbe', '0xa908558027c3e780730442c080ea5e51310bb79f55ea7d65544bc821fff01b9d', '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', '0xdc4b1fb0c6da070776aff72f3cabdd69fe1bc80b17510e8389f0fe7b99ec13b4', '0x45bc0e84058bf2d6e40391a50d54957c151ab001f998119a4a5815b008a0b2de', '0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c', '0x42ac8905f23f1485ab055f9e45c206724a7dfbefc879f6b884f9e0f952a3bfc1', '0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1', '0x02fc550c3883e5fa2c1337af5d47a1ab421de5b139ecd19f16c7e8dcb76a1955', '0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193', '0xbc3c027ad6604c5f99c79faa8fb0756a92f9a23af6eda520d99f6fec48f6cce3', '0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b', '0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220', '0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f', '0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e', '0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784', '0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb', '0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb', '0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab', '0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4', '0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f', '0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa', '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', '0xae02000000000000000000000000000000000000000000000000000000000000', '0xccf8130000000000000000000000000000000000000000000000000000000000', '0x93293d640cd7e57999f2add8910dd15145c57166082acd081ca2fbbec5cd2cbf', '0xe1be7bbc04e914d5555f015e7518c9db4668d32be20256b25f54a6094f82c759', '0xc431c70147e808fa3bbd66145251d3678b97f68913d011f0adedb116498ff7ba', '0x54649e50d85164e2d61f38905ea2f507e51812db6b9582e87d1db86c071b9983', '0x2395c64f50239f14feea5dbe13c65d405e0d4be2d25e8995d8f64b94f83014fc', '0x032ffdac4b987092a708a481a6aa53c66aa874fe96a9f689031715ffa726fee4', '0xbe6d4ac575061b5182c9112451fdc189c2d3dc3a882b4c06c365e0acded0d600', '0x6cb1b243918374de6252a32aa24a34e0e40f3df71b7b51e6a59d0e99e9109d2d', ]; const blockRoot = '0xe39be859f0aaa98d1c269252388115284366451b58ed082801593dbbfccd1876'; const slot = 11834166n; const slotTimestamp = (slot * 12n + 1606824023n) + 12n; await beaconStateVerifier.setBlockRoot(slotTimestamp, blockRoot); const correctProof = { withdrawalSlot: 11813956n, withdrawalNum: 0n, withdrawal: { index: 88947435n, validatorIndex: 688322n, withdrawalCredentials: '0x42a93a9f5cfda54716c414b6eaf07cf512f46ead', amountInGwei: 19212998n, }, witnesses: witnesses, }; assert.equal(await beaconStateVerifier.verifyWithdrawal(slotTimestamp, slot, correctProof), true); }); }); }