From 7ddbbecf07db701b86ae4280d9103070de1c73e3 Mon Sep 17 00:00:00 2001 From: Kane Wallmann Date: Thu, 1 Aug 2024 12:39:55 +1000 Subject: [PATCH] Fix for incorrect calculation of ETH matched for legacy minipools and other minor corrections --- contracts/contract/dao/RocketDAOProposal.sol | 4 +- .../protocol/RocketDAOProtocolProposal.sol | 6 +- .../protocol/RocketDAOProtocolVerifier.sol | 19 ++- .../RocketDAOProtocolSettingsAuction.sol | 8 +- .../RocketDAOProtocolSettingsProposals.sol | 10 +- .../minipool/RocketMinipoolManager.sol | 7 +- contracts/contract/node/RocketNodeStaking.sol | 31 ++++- .../RocketUpgradeOneDotThreeDotOne.sol | 122 ++++++++++++++++++ .../node/RocketNodeStakingInterface.sol | 1 + test/dao/dao-protocol-tests.js | 35 +++-- test/dao/scenario-dao-protocol.js | 10 +- test/minipool/minipool-tests.js | 18 ++- 12 files changed, 231 insertions(+), 40 deletions(-) create mode 100644 contracts/contract/upgrade/RocketUpgradeOneDotThreeDotOne.sol diff --git a/contracts/contract/dao/RocketDAOProposal.sol b/contracts/contract/dao/RocketDAOProposal.sol index 31df8c27..ea1aa0b8 100644 --- a/contracts/contract/dao/RocketDAOProposal.sol +++ b/contracts/contract/dao/RocketDAOProposal.sol @@ -16,7 +16,7 @@ contract RocketDAOProposal is RocketBase, RocketDAOProposalInterface { // 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 executer, 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) @@ -34,7 +34,7 @@ contract RocketDAOProposal is RocketBase, RocketDAOProposalInterface { // Construct constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { // Version - version = 1; + version = 2; } diff --git a/contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol b/contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol index 4c1a9d05..11e43c43 100644 --- a/contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol +++ b/contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol @@ -16,15 +16,15 @@ contract RocketDAOProtocolProposal is RocketBase, RocketDAOProtocolProposalInter 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 executer, uint256 time); - event ProposalFinalised(uint256 indexed proposalID, address indexed executer, 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 = 1; + version = 2; } /*** Proposals **********************/ diff --git a/contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol b/contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol index 6cc7b9b2..ac637b89 100644 --- a/contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol +++ b/contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol @@ -39,17 +39,17 @@ contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInter uint256 constant internal hashOffset = 2; // Burn address - address constant internal burnAddress = address(0x0000000000000000000000000000000000000000); + 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 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 = 1; + version = 2; } /// @notice Returns the depth per round @@ -157,7 +157,7 @@ contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInter // Unlock and burn RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); rocketNodeStaking.unlockRPL(proposer, proposalBond); - rocketNodeStaking.transferRPL(proposer, burnAddress, proposalBond); + rocketNodeStaking.burnRPL(proposer, proposalBond); // Log it emit ProposalBondBurned(_proposalID, proposer, proposalBond, block.timestamp); } @@ -328,16 +328,18 @@ contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInter 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.transferRPL(proposer, msg.sender, 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) /// @param _proposalID The ID of the proposal - /// @param _indices An array of indices which the challenger has a claim against + /// @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)); @@ -367,6 +369,8 @@ contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInter // 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])); @@ -386,7 +390,8 @@ contract RocketDAOProtocolVerifier is RocketBase, RocketDAOProtocolVerifierInter // Unlock the challenger bond and pay to proposer address challenger = getChallengeAddress(state); rocketNodeStaking.unlockRPL(challenger, challengeBond); - rocketNodeStaking.transferRPL(challenger, proposer, challengeBond); + rocketNodeStaking.transferRPL(challenger, proposer, challengeBond - burnPerChallenge); + rocketNodeStaking.burnRPL(challenger, burnPerChallenge); } } } diff --git a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol index c4ad5156..6befc3de 100644 --- a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol +++ b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol @@ -8,8 +8,8 @@ import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsAuc contract RocketDAOProtocolSettingsAuction is RocketDAOProtocolSettings, RocketDAOProtocolSettingsAuctionInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "auction") { - version = 2; - // Initialize settings on deployment + version = 3; + // Initialise settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Apply settings setSettingBool("auction.lot.create.enabled", true); @@ -36,8 +36,8 @@ contract RocketDAOProtocolSettingsAuction is RocketDAOProtocolSettings, RocketDA // >= 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) - require(_value >= 1 days, "Value must be >= 1 day"); + // >= 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%"); diff --git a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol index bce67e79..a77bf7cd 100644 --- a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol +++ b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol @@ -8,7 +8,7 @@ import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsPro contract RocketDAOProtocolSettingsProposals is RocketDAOProtocolSettings, RocketDAOProtocolSettingsProposalsInterface { constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, "proposals") { - version = 1; + version = 2; // Initialize settings on deployment if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) { // Init settings @@ -53,11 +53,11 @@ contract RocketDAOProtocolSettingsProposals is RocketDAOProtocolSettings, Rocket // 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 >= 51% & < 75% (RPIP-33) - require(_value >= 0.51 ether && _value < 0.75 ether, "Value must be >= 51% & < 75%"); + // Must be >= 15% & < 75% + require(_value >= 0.15 ether && _value < 0.75 ether, "Value must be >= 51% & < 75%"); } else if(settingKey == keccak256(bytes("proposal.veto.quorum"))) { - // Must be >= 51% & < 75% (RPIP-33) - require(_value >= 0.51 ether && _value < 0.75 ether, "Value must be >= 51% & < 75%"); + // Must be >= 15% & < 75% + require(_value >= 0.15 ether && _value < 0.75 ether, "Value must be >= 51% & < 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"); diff --git a/contracts/contract/minipool/RocketMinipoolManager.sol b/contracts/contract/minipool/RocketMinipoolManager.sol index c764c261..6fa4fca9 100644 --- a/contracts/contract/minipool/RocketMinipoolManager.sol +++ b/contracts/contract/minipool/RocketMinipoolManager.sol @@ -36,7 +36,7 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface { event ReductionCancelled(address indexed minipool, uint256 time); constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { - version = 4; + version = 5; } /// @notice Get the number of minipools in the network @@ -380,14 +380,15 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface { bytes32 finalisedKey = keccak256(abi.encodePacked("node.minipools.finalised", msg.sender)); require(!getBool(finalisedKey), "Minipool has already been finalised"); setBool(finalisedKey, true); + // Get ETH matched (before adding to finalised count in case of fallback calculation) + RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); + uint256 ethMatched = rocketNodeStaking.getNodeETHMatched(_nodeAddress); // 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 - RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); RocketNetworkSnapshots rocketNetworkSnapshots = RocketNetworkSnapshots(getContractAddress("rocketNetworkSnapshots")); - uint256 ethMatched = rocketNodeStaking.getNodeETHMatched(_nodeAddress); ethMatched -= RocketMinipoolInterface(msg.sender).getUserDepositBalance(); bytes32 key = keccak256(abi.encodePacked("eth.matched.node.amount", _nodeAddress)); rocketNetworkSnapshots.push(key, uint224(ethMatched)); diff --git a/contracts/contract/node/RocketNodeStaking.sol b/contracts/contract/node/RocketNodeStaking.sol index 0970e2e7..064b19e6 100644 --- a/contracts/contract/node/RocketNodeStaking.sol +++ b/contracts/contract/node/RocketNodeStaking.sol @@ -31,6 +31,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface { 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); modifier onlyRPLWithdrawalAddressOrNode(address _nodeAddress) { // Check that the call is coming from RPL withdrawal address (or node if unset) @@ -45,7 +46,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface { } constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) { - version = 5; + version = 6; // Precompute keys totalKey = keccak256(abi.encodePacked("rpl.staked.total.amount")); @@ -234,10 +235,15 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface { /// @param _nodeAddress The address of the node operator to calculate for function getNodeETHMatchedLimit(address _nodeAddress) override external view returns (uint256) { // Load contracts - RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices")); RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode")); - // Calculate & return limit + // Retrieve minimum stake parameter uint256 minimumStakePercent = rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake(); + // When minimum stake is zero, allow unlimited amount of matched ETH + if (minimumStakePercent == 0) { + return type(uint256).max; + } + // Calculate and return limit + RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices")); return getNodeRPLStake(_nodeAddress) *rocketNetworkPrices.getRPLPrice() / minimumStakePercent; } @@ -380,6 +386,25 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface { 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 onlyLatestContract("rocketNodeStaking", address(this)) onlyLatestNetworkContract() onlyRegisteredNode(_from) { + // Check sender has enough RPL + require(getNodeRPLStake(_from) >= _amount, "Node has insufficient RPL"); + // Decrease the stake amount + decreaseTotalRPLStake(_amount); + decreaseNodeRPLStake(_from, _amount); + // Withdraw the RPL to this contract + IERC20Burnable rplToken = IERC20Burnable(getContractAddress("rocketTokenRPL")); + RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); + rocketVault.withdrawToken(address(this), rplToken, _amount); + // Execute the token burn + rplToken.burn(_amount); + // Emit event + emit RPLBurned(_from, _amount, block.timestamp); + } + /// @notice Withdraw staked RPL back to the node account or withdraw RPL address /// Can only be called by a node if they have not set their RPL withdrawal address /// @param _amount The amount of RPL to withdraw diff --git a/contracts/contract/upgrade/RocketUpgradeOneDotThreeDotOne.sol b/contracts/contract/upgrade/RocketUpgradeOneDotThreeDotOne.sol new file mode 100644 index 00000000..2a698c06 --- /dev/null +++ b/contracts/contract/upgrade/RocketUpgradeOneDotThreeDotOne.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.18; + +import "../RocketBase.sol"; +import "../../interface/network/RocketNetworkSnapshotsInterface.sol"; +import "../../interface/network/RocketNetworkPricesInterface.sol"; +import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; +import "../../interface/util/AddressSetStorageInterface.sol"; +import "../../interface/minipool/RocketMinipoolManagerInterface.sol"; + +/// @notice v1.3.1 hotfix upgrade contract +contract RocketUpgradeOneDotThreeDotOne is RocketBase { + + // Whether the upgrade has been performed or not + bool public executed; + + // Whether the contract is locked to further changes + bool public locked; + + // Upgrade contracts + address public newRocketDAOProposal; + address public newRocketDAOProtocolProposal; + address public newRocketDAOProtocolVerifier; + address public newRocketDAOProtocolSettingsProposals; + address public newRocketDAOProtocolSettingsAuction; + address public newRocketMinipoolManager; + address public newRocketNodeStaking; + + // Upgrade ABIs + string public newRocketDAOProposalAbi; + string public newRocketDAOProtocolProposalAbi; + string public newRocketDAOProtocolVerifierAbi; + string public newRocketDAOProtocolSettingsProposalsAbi; + string public newRocketDAOProtocolSettingsAuctionAbi; + string public newRocketMinipoolManagerAbi; + string public newRocketNodeStakingAbi; + + // Save deployer to limit access to set functions + address immutable deployer; + + // Construct + constructor( + RocketStorageInterface _rocketStorageAddress + ) RocketBase(_rocketStorageAddress) { + // Version + version = 1; + deployer = msg.sender; + } + + /// @notice Returns the address of the RocketStorage contract + function getRocketStorageAddress() external view returns (address) { + return address(rocketStorage); + } + + function set(address[] memory _addresses, string[] memory _abis) external { + require(msg.sender == deployer, "Only deployer"); + require(!locked, "Contract locked"); + + // Set contract addresses + newRocketDAOProposal = _addresses[0]; + newRocketDAOProtocolProposal = _addresses[1]; + newRocketDAOProtocolVerifier = _addresses[2]; + newRocketDAOProtocolSettingsProposals = _addresses[3]; + newRocketDAOProtocolSettingsAuction = _addresses[4]; + newRocketMinipoolManager = _addresses[5]; + newRocketNodeStaking = _addresses[6]; + + // Set ABIs + newRocketDAOProposalAbi = _abis[0]; + newRocketDAOProtocolProposalAbi = _abis[1]; + newRocketDAOProtocolVerifierAbi = _abis[2]; + newRocketDAOProtocolSettingsProposalsAbi = _abis[3]; + newRocketDAOProtocolSettingsAuctionAbi = _abis[4]; + newRocketMinipoolManagerAbi = _abis[5]; + newRocketNodeStakingAbi = _abis[6]; + } + + /// @notice Prevents further changes from being applied + function lock() external { + require(msg.sender == deployer, "Only deployer"); + locked = true; + } + + /// @notice Once this contract has been voted in by oDAO, guardian can perform the upgrade + function execute() external onlyGuardian { + require(!executed, "Already executed"); + executed = true; + + // Upgrade contracts + _upgradeContract("rocketDAOProposal", newRocketDAOProposal, newRocketDAOProposalAbi); + _upgradeContract("rocketDAOProtocolProposal", newRocketDAOProtocolProposal, newRocketDAOProtocolProposalAbi); + _upgradeContract("rocketDAOProtocolVerifier", newRocketDAOProtocolVerifier, newRocketDAOProtocolVerifierAbi); + _upgradeContract("rocketDAOProtocolSettingsProposals", newRocketDAOProtocolSettingsProposals, newRocketDAOProtocolSettingsProposalsAbi); + _upgradeContract("rocketDAOProtocolSettingsAuction", newRocketDAOProtocolSettingsAuction, newRocketDAOProtocolSettingsAuctionAbi); + _upgradeContract("rocketMinipoolManager", newRocketMinipoolManager, newRocketMinipoolManagerAbi); + _upgradeContract("rocketNodeStaking", newRocketNodeStaking, newRocketNodeStakingAbi); + + // Set a protocol version value in storage for convenience with bindings + setString(keccak256(abi.encodePacked("protocol.version")), "1.3.1"); + } + + /// @dev Upgrade a network contract + function _upgradeContract(string memory _name, address _contractAddress, string memory _contractAbi) internal { + // 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))); + } +} \ No newline at end of file diff --git a/contracts/interface/node/RocketNodeStakingInterface.sol b/contracts/interface/node/RocketNodeStakingInterface.sol index cad2f632..8fd4bc9e 100644 --- a/contracts/interface/node/RocketNodeStakingInterface.sol +++ b/contracts/interface/node/RocketNodeStakingInterface.sol @@ -22,6 +22,7 @@ interface RocketNodeStakingInterface { 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 withdrawRPL(uint256 _amount) external; function withdrawRPL(address _nodeAddress, uint256 _amount) external; function slashRPL(address _nodeAddress, uint256 _ethSlashAmount) external; diff --git a/test/dao/dao-protocol-tests.js b/test/dao/dao-protocol-tests.js index 4dbee921..7f992da1 100644 --- a/test/dao/dao-protocol-tests.js +++ b/test/dao/dao-protocol-tests.js @@ -110,6 +110,18 @@ export default function() { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.maximum.count', 100, {from: owner}); }); + // + // Utilities + // + + function burnAmount(bond) { + return bond.mul('0.2'.ether).div('1'.ether) + } + + function bondAfterBurn(bond) { + return bond.sub(burnAmount(bond)); + } + // // Start Tests // @@ -525,6 +537,7 @@ export default function() { const deltas = await daoProtocolClaimBondProposer(propId, [1], { from: proposer }); assertBN.equal(deltas.locked, proposalBond.neg()); assertBN.equal(deltas.staked, '0'.BN); + assertBN.equal(deltas.burned, '0'.BN); }); it(printTitle('proposer', 'can successfully claim invalid challenge'), async () => { @@ -551,12 +564,6 @@ export default function() { // Response let pollard = await daoProtocolGeneratePollard(leaves, depthPerRound, index); await daoProtocolSubmitRoot(propId, index, pollard, { from: proposer }); - - // // Challenge - // await daoProtocolCreateChallenge(propId, index, { from: challenger }); - // // Response - // let response = await daoProtocolGeneratePollard(leaves, depthPerRound, index); - // await daoProtocolSubmitRoot(propId, index, response.proof, response.nodes, { from: proposer }); } // Wait for proposal wait period to end @@ -568,7 +575,8 @@ export default function() { // Claim bond and rewards const deltas = await daoProtocolClaimBondProposer(propId, [1, ...indices], { from: proposer }); assertBN.equal(deltas.locked, proposalBond.neg()); - assertBN.equal(deltas.staked, challengeBond.mul(indices.length.toString().BN)); + assertBN.equal(deltas.staked, bondAfterBurn(challengeBond).mul(indices.length.toString().BN)); + assertBN.equal(deltas.burned, burnAmount(challengeBond).mul(indices.length.toString().BN)); }); it(printTitle('proposer', 'can not withdraw excess RPL if it is locked'), async () => { @@ -1612,11 +1620,13 @@ export default function() { 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 - assertBN.equal(deltas1.staked, proposalBond.div('2'.BN)); - assertBN.equal(deltas2.staked, proposalBond.div('2'.BN)); + // 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.div('2'.BN))); + assertBN.equal(deltas2.staked, bondAfterBurn(proposalBond.div('2'.BN))); assertBN.equal(deltas1.locked, challengeBond.neg()); assertBN.equal(deltas2.locked, challengeBond.neg()); + assertBN.equal(deltas1.burned, burnAmount(proposalBond.div('2'.BN))); + assertBN.equal(deltas2.burned, burnAmount(proposalBond.div('2'.BN))); }); it(printTitle('challenger', 'can recover bond if index was not used'), async () => { @@ -1657,9 +1667,11 @@ export default function() { const deltas2 = await daoProtocolClaimBondChallenger(propId, [index + 1], { from: challenger2 }); assertBN.equal(deltas1.locked, challengeBond.neg()); - assertBN.equal(deltas1.staked, proposalBond); + assertBN.equal(deltas1.staked, bondAfterBurn(proposalBond)); + assertBN.equal(deltas1.burned, burnAmount(proposalBond)); assertBN.equal(deltas2.locked, challengeBond.neg()); assertBN.equal(deltas2.staked, '0'.BN); + assertBN.equal(deltas2.burned, '0'.BN); }); it(printTitle('challenger', 'can recover bond if proposal was successful'), async () => { @@ -1696,6 +1708,7 @@ export default function() { assertBN.equal(deltas.locked, challengeBond.neg()); assertBN.equal(deltas.staked, '0'.BN); + assertBN.equal(deltas.burned, '0'.BN); }); it(printTitle('challenger', 'can not create challenge with proof from a deeper index'), async () => { diff --git a/test/dao/scenario-dao-protocol.js b/test/dao/scenario-dao-protocol.js index 478fccdc..e23dd032 100644 --- a/test/dao/scenario-dao-protocol.js +++ b/test/dao/scenario-dao-protocol.js @@ -2,7 +2,7 @@ import { RocketNetworkVoting, RocketDAOProtocolVerifier, RocketDAOProtocolSettingsProposals, - RocketDAOProtocolProposal, RocketNodeManager, RocketNodeStaking, + RocketDAOProtocolProposal, RocketNodeManager, RocketNodeStaking, RocketTokenRPL, } from '../_utils/artifacts'; import { assertBN } from '../_helpers/bn'; import { voteStates } from './scenario-dao-proposal'; @@ -706,35 +706,43 @@ export async function daoProtocolFinalise(_proposalID, txOptions) { export async function daoProtocolClaimBondProposer(_proposalID, _indices, txOptions) { const rocketDAOProtocolVerifier = await RocketDAOProtocolVerifier.deployed(); const rocketNodeStaking = await RocketNodeStaking.deployed(); + const rocketTokenRPL = await RocketTokenRPL.deployed(); const lockedBalanceBefore = await rocketNodeStaking.getNodeRPLLocked(txOptions.from); const balanceBefore = await rocketNodeStaking.getNodeRPLStake(txOptions.from); + const supplyBefore = await rocketTokenRPL.totalSupply(); await rocketDAOProtocolVerifier.claimBondProposer(_proposalID, _indices, txOptions); const lockedBalanceAfter = await rocketNodeStaking.getNodeRPLLocked(txOptions.from); const balanceAfter = await rocketNodeStaking.getNodeRPLStake(txOptions.from); + const supplyAfter = await rocketTokenRPL.totalSupply(); return { staked: balanceAfter.sub(balanceBefore), locked: lockedBalanceAfter.sub(lockedBalanceBefore), + burned: supplyBefore.sub(supplyAfter), } } export async function daoProtocolClaimBondChallenger(_proposalID, _indices, txOptions) { const rocketDAOProtocolVerifier = await RocketDAOProtocolVerifier.deployed(); const rocketNodeStaking = await RocketNodeStaking.deployed(); + const rocketTokenRPL = await RocketTokenRPL.deployed(); const lockedBalanceBefore = await rocketNodeStaking.getNodeRPLLocked(txOptions.from); const balanceBefore = await rocketNodeStaking.getNodeRPLStake(txOptions.from); + const supplyBefore = await rocketTokenRPL.totalSupply(); await rocketDAOProtocolVerifier.claimBondChallenger(_proposalID, _indices, txOptions); const lockedBalanceAfter = await rocketNodeStaking.getNodeRPLLocked(txOptions.from); const balanceAfter = await rocketNodeStaking.getNodeRPLStake(txOptions.from); + const supplyAfter = await rocketTokenRPL.totalSupply(); return { staked: balanceAfter.sub(balanceBefore), locked: lockedBalanceAfter.sub(lockedBalanceBefore), + burned: supplyBefore.sub(supplyAfter), } } diff --git a/test/minipool/minipool-tests.js b/test/minipool/minipool-tests.js index 444a7294..2e585435 100644 --- a/test/minipool/minipool-tests.js +++ b/test/minipool/minipool-tests.js @@ -8,7 +8,11 @@ import { RocketDAONodeTrustedSettingsMinipool, RocketMinipoolBase, RocketMinipoolBondReducer, - RocketDAOProtocolSettingsRewards, RocketNodeManager, RocketMinipoolDelegate, RocketNodeDistributorFactory, + RocketDAOProtocolSettingsRewards, + RocketNodeManager, + RocketMinipoolDelegate, + RocketNodeDistributorFactory, + RocketDAOProtocolSettingsNode, } from '../_utils/artifacts'; import { increaseTime } from '../_utils/evm'; import { printTitle } from '../_utils/formatting'; @@ -932,6 +936,18 @@ export default function() { await increaseTime(web3, 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