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 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
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);
});
});
}