Skip to content

Commit

Permalink
Hot fix for Houston disparities
Browse files Browse the repository at this point in the history
  • Loading branch information
kanewallmann committed May 8, 2024
1 parent 84ac198 commit e760442
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract RocketDAOProtocolSettingsSecurity is RocketDAOProtocolSettings, RocketD
bytes32 settingKey = keccak256(abi.encodePacked(_settingPath));
if(settingKey == keccak256(abi.encodePacked("members.quorum"))) {
// >= 51% & < 75% (RPIP-33)
require(_value >= 0.51 ether && _value <= 0.75 ether, "Quorum setting must be >= 51% & < 75%");
require(_value >= 0.51 ether && _value <= 0.75 ether, "Quorum setting must be >= 51% & <= 75%");
} else if(settingKey == keccak256(abi.encodePacked("members.leave.time"))) {
// < 14 days (RPIP-33)
require(_value < 14 days, "Value must be < 14 days");
Expand Down
14 changes: 12 additions & 2 deletions contracts/contract/helper/RevertOnTransfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ pragma solidity 0.7.6;

// SPDX-License-Identifier: GPL-3.0-only

// Helper contract to simulate malicious node withdrawal address
// Helper contract to simulate malicious node withdrawal address or withdrawal address
contract RevertOnTransfer {
bool public enabled = true;

function setEnabled(bool _enabled) external {
enabled = _enabled;
}

fallback() external payable {
revert();
require(!enabled);
}

function call(address _address, bytes calldata _payload) external payable {
_address.call{value: msg.value}(_payload);
}
}
48 changes: 41 additions & 7 deletions contracts/contract/rewards/RocketMerkleDistributorMainnet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import "../../interface/rewards/RocketRewardsRelayInterface.sol";
import "../../interface/rewards/RocketSmoothingPoolInterface.sol";
import "../../interface/RocketVaultWithdrawerInterface.sol";
import "../../interface/node/RocketNodeManagerInterface.sol";
import "../../interface/rewards/RocketMerkleDistributorMainnetInterface.sol";

import "@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, RocketRewardsRelayInterface, RocketVaultWithdrawerInterface {
contract RocketMerkleDistributorMainnet is RocketBase, RocketMerkleDistributorMainnetInterface, RocketVaultWithdrawerInterface {

// Events
event RewardsClaimed(address indexed claimer, uint256[] rewardIndex, uint256[] amountRPL, uint256[] amountETH);
Expand Down Expand Up @@ -79,10 +80,15 @@ contract RocketMerkleDistributorMainnet is RocketBase, RocketRewardsRelayInterfa
rplWithdrawalAddress = rocketNodeManager.getNodeRPLWithdrawalAddress(_nodeAddress);
withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress);
if (rocketNodeManager.getNodeRPLWithdrawalAddressIsSet(_nodeAddress)) {
// If RPL withdrawal address is set, must be called from it
require(msg.sender == rplWithdrawalAddress, "Can only claim from RPL withdrawal address");
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 {
// Otherwise, must be called from node address or withdrawal address
// 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");
}
}
Expand All @@ -109,8 +115,15 @@ contract RocketMerkleDistributorMainnet is RocketBase, RocketRewardsRelayInterfa
// Distribute ETH
if (totalAmountETH > 0) {
rocketVault.withdrawEther(totalAmountETH);
(bool result,) = withdrawalAddress.call{value: totalAmountETH}("");
require(result, "Failed to claim ETH");
// Allow up to 2300 gas to send ETH to the withdrawal address
(bool result,) = withdrawalAddress.call{value: totalAmountETH, gas: 2300}("");
if (!result) {
// If the withdrawal address cannot accept the ETH with 2300 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, totalAmountETH);
// Return the ETH to the vault
rocketVault.depositEther{value: totalAmountETH}();
}
}
}

Expand All @@ -127,6 +140,27 @@ contract RocketMerkleDistributorMainnet is RocketBase, RocketRewardsRelayInterfa
emit RewardsClaimed(_nodeAddress, _rewardIndex, _amountRPL, _amountETH);
}

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

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

// Verifies the given data exists as a leaf nodes for the specified reward interval and marks them as claimed if they are valid
// Note: this function is optimised for gas when _rewardIndex is ordered numerically
function _verifyClaim(uint256[] calldata _rewardIndex, address _nodeAddress, uint256[] calldata _amountRPL, uint256[] calldata _amountETH, bytes32[][] calldata _merkleProof) internal {
Expand Down Expand Up @@ -161,7 +195,7 @@ contract RocketMerkleDistributorMainnet is RocketBase, RocketRewardsRelayInterfa
setUint(claimedWordKey, claimedWord);
}

// Verifies that the
// Verifies that the given proof is valid
function _verifyProof(uint256 _rewardIndex, address _nodeAddress, uint256 _amountRPL, uint256 _amountETH, bytes32[] calldata _merkleProof) internal view returns (bool) {
bytes32 node = keccak256(abi.encodePacked(_nodeAddress, network, _amountRPL, _amountETH));
bytes32 key = keccak256(abi.encodePacked('rewards.merkle.root', _rewardIndex));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >0.5.0 <0.9.0;

import "./RocketRewardsRelayInterface.sol";

interface RocketMerkleDistributorMainnetInterface is RocketRewardsRelayInterface {
function claimOutstandingEth() external;
function getOutstandingEth(address _address) external view returns (uint256);
}
130 changes: 128 additions & 2 deletions test/rewards/rewards-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { getCurrentTime, increaseTime } from '../_utils/evm'
import { printTitle } from '../_utils/formatting';
import { shouldRevert } from '../_utils/testing';
import { submitPrices } from '../_helpers/network';
import { bufferToHex, keccak256 } from 'ethereumjs-util';
import {
registerNode,
setNodeTrusted,
setNodeWithdrawalAddress,
nodeStakeRPL,
getNodeEffectiveRPLStake,
} from '../_helpers/node'
getNodeEffectiveRPLStake, setNodeRPLWithdrawalAddress,
} from '../_helpers/node';
import {
RevertOnTransfer,
RocketDAONodeTrustedSettingsMinipool,
RocketDAOProtocolSettingsNode,
RocketMerkleDistributorMainnet,
Expand Down Expand Up @@ -533,6 +535,77 @@ export default function() {
});


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 = await getCurrentTime(web3);
assert.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time');
await increaseTime(web3, rplInflationStartTime - currentTime + claimIntervalTime);

// Submit rewards snapshot
const rewards = [
{
address: registeredNode1,
network: 0,
trustedNodeRPL: '0'.ether,
nodeRPL: '1'.ether,
nodeETH: '0'.ether
}
]
await submitRewards(0, rewards, '0'.ether, '0'.ether, {from: registeredNodeTrusted1});
await submitRewards(0, rewards, '0'.ether, '0'.ether, {from: registeredNodeTrusted2});

// Can't claim from node address
await shouldRevert(claimAndStakeRewards(registeredNode1, [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, [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, [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 = await getCurrentTime(web3);
assert.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time');
await increaseTime(web3, rplInflationStartTime - currentTime + claimIntervalTime);

// Submit rewards snapshot
const rewards = [
{
address: registeredNode1,
network: 0,
trustedNodeRPL: '0'.ether,
nodeRPL: '1'.ether,
nodeETH: '0'.ether
}
]
await submitRewards(0, rewards, '0'.ether, '0'.ether, {from: registeredNodeTrusted1});
await submitRewards(0, rewards, '0'.ether, '0'.ether, {from: registeredNodeTrusted2});

// Can claim from withdrawal address
await claimAndStakeRewards(registeredNode1, [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();
Expand Down Expand Up @@ -764,5 +837,58 @@ export default function() {
}), '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.address, { from: node1WithdrawalAddress })

// Move to inflation start plus one claim interval
let currentTime = await getCurrentTime(web3);
assert.isBelow(currentTime, rplInflationStartTime, 'Current block should be below RPL inflation start time');
await increaseTime(web3, rplInflationStartTime - currentTime + claimIntervalTime);

// Send ETH to rewards pool
const rocketSmoothingPool = await RocketSmoothingPool.deployed();
await web3.eth.sendTransaction({ from: owner, to: rocketSmoothingPool.address, value: '20'.ether});

// Submit rewards snapshot
const rewards = [
{
address: registeredNode1,
network: 0,
trustedNodeRPL: '0'.ether,
nodeRPL: '0'.ether,
nodeETH: '1'.ether
}
]
await submitRewards(0, rewards, '0'.ether, '1'.ether, {from: registeredNodeTrusted1});
await submitRewards(0, rewards, '0'.ether, '1'.ether, {from: registeredNodeTrusted2});

// Claim from node which should fail to send the ETH and increase outstanding balance
await claimAndStakeRewards(registeredNode1, [0], [rewards], '0'.ether, {
from: registeredNode1,
});

const rocketMerkleDistributorMainnet = await RocketMerkleDistributorMainnet.deployed();

// Check outstanding balance is correct
const balance = await rocketMerkleDistributorMainnet.getOutstandingEth(revertOnTransfer.address);
assertBN.equal(balance, '1'.ether);

// Attempt to claim the ETH from the previously reverting withdrawal address
await revertOnTransfer.setEnabled(false);
const payload = '0x' + keccak256('claimOutstandingEth()').toString('hex').substring(0,8);
await revertOnTransfer.call(rocketMerkleDistributorMainnet.address, payload);

// Check ETH was sent to withdrawal address
const withdrawalAddressBalance = (await web3.eth.getBalance(revertOnTransfer.address)).BN;
assertBN.equal(withdrawalAddressBalance, '1'.ether);
})
});
}
11 changes: 7 additions & 4 deletions test/rewards/scenario-claim-and-stake-rewards.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export async function claimAndStakeRewards(nodeAddress, indices, rewards, stakeA
rocketRewardsPool.getClaimIntervalTimeStart(),
rocketTokenRPL.balanceOf.call(nodeWithdrawalAddress),
rocketNodeStaking.getNodeRPLStake(nodeAddress),
web3.eth.getBalance(nodeWithdrawalAddress)
web3.eth.getBalance(nodeWithdrawalAddress),
rocketMerkleDistributorMainnet.getOutstandingEth(nodeWithdrawalAddress),
]).then(
([claimIntervalTimeStart, nodeRpl, rplStake, nodeEth]) =>
({claimIntervalTimeStart, nodeRpl, rplStake, nodeEth: web3.utils.toBN(nodeEth)})
([claimIntervalTimeStart, nodeRpl, rplStake, nodeEth, outstandingEth]) =>
({claimIntervalTimeStart, nodeRpl, rplStake, nodeEth: web3.utils.toBN(nodeEth), outstandingEth: web3.utils.toBN(outstandingEth)})
);
}

Expand Down Expand Up @@ -75,6 +76,7 @@ export async function claimAndStakeRewards(nodeAddress, indices, rewards, stakeA
proofs.push(proof.proof);

totalAmountRPL = totalAmountRPL.add(web3.utils.toBN(proof.amountRPL));
totalAmountETH = totalAmountETH.add(web3.utils.toBN(proof.amountETH));
}

const tx = await rocketMerkleDistributorMainnet.claimAndStake(nodeAddress, indices, amountsRPL, amountsETH, proofs, stakeAmount, txOptions);
Expand All @@ -91,5 +93,6 @@ export async function claimAndStakeRewards(nodeAddress, indices, rewards, stakeA
let amountStaked = balances2.rplStake.sub(balances1.rplStake);

assertBN.equal(balances2.nodeRpl.sub(balances1.nodeRpl), totalAmountRPL.sub(amountStaked), 'Incorrect updated node RPL balance');
assertBN.equal(balances2.nodeEth.sub(balances1.nodeEth).add(gasUsed), totalAmountETH, 'Incorrect updated node ETH balance');
const ethDiff = balances2.nodeEth.sub(balances1.nodeEth).add(gasUsed).add(balances2.outstandingEth.sub(balances1.outstandingEth));
assertBN.equal(ethDiff, totalAmountETH, 'Incorrect updated node ETH balance');
}

0 comments on commit e760442

Please sign in to comment.