diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5436a0e9..af338b55 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "solidity.packageDefaultDependenciesContractsDirectory": ""
-}
\ No newline at end of file
+ "solidity.packageDefaultDependenciesContractsDirectory": "",
+ "solidity.compileUsingRemoteVersion": "v0.5.17+commit.d19bba13"
+}
diff --git a/contracts/Bindings.sol b/contracts/Bindings.sol
index 1b1b281e..6c559846 100644
--- a/contracts/Bindings.sol
+++ b/contracts/Bindings.sol
@@ -1,14 +1,8 @@
pragma solidity 0.5.17;
-import "./DarknodePayment/ClaimRewards.sol";
-import "./DarknodePayment/DarknodePayment.sol";
-import "./DarknodePayment/DarknodePaymentStore.sol";
import "./DarknodeRegistry/DarknodeRegistry.sol";
import "./DarknodeRegistry/DarknodeRegistryStore.sol";
-import "./DarknodeRegistry/GetOperatorDarknodes.sol";
-import "./DarknodeSlasher/DarknodeSlasher.sol";
import "./RenToken/RenToken.sol";
-import "./Protocol/Protocol.sol";
/// @notice Bindings imports all of the contracts for generating bindings.
/* solium-disable-next-line no-empty-blocks */
diff --git a/contracts/DarknodePayment/ClaimRewards.sol b/contracts/DarknodePayment/ClaimRewards.sol
deleted file mode 100644
index 30932f18..00000000
--- a/contracts/DarknodePayment/ClaimRewards.sol
+++ /dev/null
@@ -1,145 +0,0 @@
-//SPDX-License-Identifier: MIT
-
-pragma solidity 0.5.17;
-
-import "./ValidString.sol";
-
-contract ClaimRewards {
- uint256 public constant BPS_DENOMINATOR = 10000;
-
- event LogClaimRewards(
- address indexed operatorAddress_,
- string assetSymbol_,
- string recipientAddress_,
- string recipientChain_,
- bytes recipientPayload_,
- uint256 fractionInBps_,
- // Repeated values for indexing.
- string indexed assetSymbolIndexed_,
- string indexed recipientAddressIndexed_
- );
-
- modifier validFractionInBps(uint256 fraction_) {
- require(
- fraction_ <= BPS_DENOMINATOR,
- "ClaimRewards: invalid fractionInBps"
- );
- _;
- }
-
- /**
- * claimRewardsToChain allows darknode operators to withdraw darknode
- * earnings, as an on-chain alternative to the JSON-RPC claim method.
- *
- * It will the operators total sum of rewards, for all of their nodes.
- *
- * @param assetSymbol_ The token symbol.
- * E.g. "BTC", "DOGE" or "FIL".
- * @param recipientAddress_ An address on the asset's native chain, for
- * receiving the withdrawn rewards. This should be a string as
- * provided by the user - no encoding or decoding required.
- * E.g.: "miMi2VET41YV1j6SDNTeZoPBbmH8B4nEx6" for BTC.
- * @param recipientChain_ A string indicating which chain the rewards should
- * be withdrawn to. It should be the name of the chain as expected by
- * RenVM (e.g. "Ethereum" or "Solana"). Support for different chains
- * will be rolled out after this contract is deployed, starting with
- * "Ethereum", then other host chains (e.g. "Polygon" or "Solana")
- * and then lock chains (e.g. "Bitcoin" for "BTC"), also represented
- * by an empty string "".
- * @param recipientPayload_ An associated payload that can be provided along
- * with the recipient chain and address. Should be empty if not
- * required.
- * @param fractionInBps_ A value between 0 and 10000 (inclusive) that
- * indicates the percent to withdraw from each of the operator's
- * darknodes. The value should be in BPS, meaning 10000 represents
- * 100%, 5000 represents 50%, etc.
- */
- function claimRewardsToChain(
- string memory assetSymbol_,
- string memory recipientAddress_,
- string memory recipientChain_,
- bytes memory recipientPayload_,
- uint256 fractionInBps_
- ) public validFractionInBps(fractionInBps_) {
- // Validate asset symbol.
- require(
- ValidString.isNotEmpty(assetSymbol_),
- "ClaimRewards: invalid empty asset"
- );
- require(
- ValidString.isAlphanumeric(assetSymbol_),
- "ClaimRewards: invalid asset"
- );
-
- // Validate recipient address.
- require(
- ValidString.isNotEmpty(recipientAddress_),
- "ClaimRewards: invalid empty recipient address"
- );
- require(
- ValidString.isAlphanumeric(recipientAddress_),
- "ClaimRewards: invalid recipient address"
- );
-
- // Validate recipient chain.
- // Note that the chain can be empty - which is planned to represent the
- // asset's native lock chain.
- require(
- ValidString.isAlphanumeric(recipientChain_),
- "ClaimRewards: invalid recipient chain"
- );
-
- address operatorAddress = msg.sender;
-
- // Emit event.
- emit LogClaimRewards(
- operatorAddress,
- assetSymbol_,
- recipientAddress_,
- recipientChain_,
- recipientPayload_,
- fractionInBps_,
- // Indexed
- assetSymbol_,
- recipientAddress_
- );
- }
-
- /**
- * `claimRewardsToEthereum` calls `claimRewardsToChain` internally
- */
- function claimRewardsToEthereum(
- string memory assetSymbol_,
- address recipientAddress_,
- uint256 fractionInBps_
- ) public {
- return
- claimRewardsToChain(
- assetSymbol_,
- addressToString(recipientAddress_),
- "Ethereum",
- "",
- fractionInBps_
- );
- }
-
- // From https://ethereum.stackexchange.com/questions/8346/convert-address-to-string
- function addressToString(address address_)
- public
- pure
- returns (string memory)
- {
- bytes memory data = abi.encodePacked(address_);
-
- bytes memory alphabet = "0123456789abcdef";
-
- bytes memory str = new bytes(2 + data.length * 2);
- str[0] = "0";
- str[1] = "x";
- for (uint256 i = 0; i < data.length; i++) {
- str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))];
- str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))];
- }
- return string(str);
- }
-}
diff --git a/contracts/DarknodePayment/ClaimlessRewards.sol b/contracts/DarknodePayment/ClaimlessRewards.sol
deleted file mode 100644
index 21d9049a..00000000
--- a/contracts/DarknodePayment/ClaimlessRewards.sol
+++ /dev/null
@@ -1,706 +0,0 @@
-pragma solidity ^0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-
-import "../DarknodeRegistry/DarknodeRegistry.sol";
-import "./DarknodePaymentStore.sol";
-import "../libraries/LinkedList.sol";
-import "../Governance/Claimable.sol";
-
-contract ClaimlessRewardsEvents {
- /// @notice Emitted when a node calls withdraw
- /// @param _payee The address of the node which withdrew
- /// @param _value The amount of DAI withdrawn
- /// @param _token The address of the token that was withdrawn
- event LogDarknodeWithdrew(
- address indexed _payee,
- uint256 _value,
- address indexed _token
- );
-
- /// @notice Emitted when the cycle is changed.
- /// @param _newTimestamp The start timestamp of the new cycle.
- /// @param _previousTimestamp The start timestamp of the previous cycle.
- /// @param _shareCount The number of darknodes at the end of the previous
- /// cycle.
- event LogCycleChanged(
- uint256 _newTimestamp,
- uint256 _previousTimestamp,
- uint256 _shareCount
- );
-
- /// @notice Emitted when the node payout percent changes.
- /// @param _newNumerator The new numerator.
- /// @param _oldNumerator The old numerator.
- event LogHourlyPayoutChanged(uint256 _newNumerator, uint256 _oldNumerator);
-
- /// @notice Emitted when the community fund percent changes.
- /// @param _newNumerator The new numerator.
- /// @param _oldNumerator The old numerator.
- event LogCommunityFundNumeratorChanged(
- uint256 _newNumerator,
- uint256 _oldNumerator
- );
-
- /// @notice Emitted when a new token is registered.
- /// @param _token The token that was registered.
- event LogTokenRegistered(address indexed _token);
-
- /// @notice Emitted when a token is deregistered.
- /// @param _token The token that was deregistered.
- event LogTokenDeregistered(address indexed _token);
-
- /// @notice Emitted when the DarknodeRegistry is updated.
- /// @param _previousDarknodeRegistry The address of the old registry.
- /// @param _nextDarknodeRegistry The address of the new registry.
- event LogDarknodeRegistryUpdated(
- DarknodeRegistryLogicV1 indexed _previousDarknodeRegistry,
- DarknodeRegistryLogicV1 indexed _nextDarknodeRegistry
- );
-
- /// @notice Emitted when the community fund recipient is updated.
- /// @param _previousCommunityFund The address of the old community fund.
- /// @param _nextCommunityFund The address of the new community fund.
- event LogCommunityFundUpdated(
- address indexed _previousCommunityFund,
- address indexed _nextCommunityFund
- );
-}
-
-contract ClaimlessRewardsState {
- using LinkedList for LinkedList.List;
-
- /// @notice The special address for the collective funds that haven't been
- /// withdrawn yet.
- address public constant POOLED_REWARDS = address(0);
-
- /// @notice The address of the Darknode Registry, used to look up the
- /// operators of nodes and the number of registered nodes.
- /// Passed in as a constructor parameter.
- DarknodeRegistryLogicV1 public darknodeRegistry;
-
- /// @notice DarknodePaymentStore stores the rewards until they are paid out.
- DarknodePaymentStore public store; // Passed in as a constructor parameter.
-
- /// @notice Mapping from token -> amount.
- /// The amount of rewards allocated for each node.
- mapping(uint256 => mapping(address => uint256))
- public cycleCumulativeTokenShares;
-
- /// @notice Mapping of node -> token -> last claimed timestamp
- /// Used to keep track of which nodes have already claimed their rewards.
- mapping(address => mapping(address => uint256)) public rewardsLastClaimed;
-
- uint256 public latestCycleTimestamp;
- uint256[] public epochTimestamps;
-
- /// @notice The list of tokens which are already registered and rewards can
- /// be claimed for.
- LinkedList.List internal registeredTokens;
-
- /// @notice The list of deregistered tokens, tracked to ensure that users
- /// can still withdraw rewards for tokens that have been deregistered.
- LinkedList.List internal deregisteredTokens;
-
- /// @notice The recipient of a proportion of rewards. The proportion can be
- /// updated, allowing this to be governed by a DAO.
- address public communityFund;
-
- /// @notice The denominator used by `hourlyPayoutWithheldNumerator` and
- /// `communityFundNumerator`.
- uint256 public constant HOURLY_PAYOUT_WITHHELD_DENOMINATOR = 1000000;
-
- /// @notice The proportion of the payout that is withheld each hour to be
- /// paid out over future cycles.
- /// The target payout is 50% over 28 days, so the following was calculated
- /// as `0.5 ** (1 / (28 * 24))`.
- /// Rounding is done in favor of the current cycle payout instead of the
- /// rewards withheld for future cycles.
- uint256 public hourlyPayoutWithheldNumerator = 998969;
-
- /// @notice The proportion of the reward pool that goes to the community
- /// fund. `communityFundNumerator` can be set to 0 to disable rewards going
- /// to the fund.
- uint256 public communityFundNumerator;
-}
-
-contract ClaimlessRewardsTokenHandler is
- Ownable,
- ClaimlessRewardsEvents,
- ClaimlessRewardsState
-{
- using SafeERC20 for ERC20;
-
- /// @notice Adds tokens to be payable. Registration is pending until next
- /// cycle.
- ///
- /// @param _token The address of the token to be registered.
- function registerToken(address _token) external onlyOwner {
- require(
- !registeredTokens.isInList(_token),
- "ClaimlessRewards: token already registered"
- );
- registeredTokens.append(_token);
-
- // Remove from deregistered tokens.
- if (deregisteredTokens.isInList(_token)) {
- deregisteredTokens.remove(_token);
- }
- }
-
- /// @notice Removes a token from the list of supported tokens.
- /// Deregistration is pending until next cycle.
- ///
- /// @param _token The address of the token to be deregistered.
- function deregisterToken(address _token) external onlyOwner {
- require(
- registeredTokens.isInList(_token),
- "ClaimlessRewards: token not registered"
- );
- registeredTokens.remove(_token);
-
- // Add to deregistered tokens. This check should always be true.
- if (!deregisteredTokens.isInList(_token)) {
- deregisteredTokens.append(_token);
- }
- }
-
- /// @notice (external view) Returns the full list of registered tokens.
- function getRegisteredTokens() external view returns (address[] memory) {
- address[] memory tokens = new address[](registeredTokens.length);
- address nextToken = registeredTokens.begin();
- for (uint256 i = 0; i < tokens.length; i++) {
- tokens[i] = nextToken;
-
- // Take next token.
- nextToken = registeredTokens.next(nextToken);
- }
- return tokens;
- }
-
- /// @notice (external view) Returns whether a token is registered.
- function isRegistered(address _token) external view returns (bool) {
- return registeredTokens.isInList(_token);
- }
-
- /// @notice Forwards any balance held by this contract on to the store.
- ///
- /// @param _token The token to forward. For ETH, this is 0xeeee... .
- function forward(address _token) external {
- // If no token has been provided, forward ETH.
- if (_token == address(0x0)) {
- address(store).transfer(address(this).balance);
- } else {
- ERC20(_token).safeTransfer(
- address(store),
- ERC20(_token).balanceOf(address(this))
- );
- }
- }
-}
-
-contract ClaimlessRewardsAdminHandler is
- Ownable,
- ClaimlessRewardsEvents,
- ClaimlessRewardsState
-{
- /// @notice Allows the contract owner to update the address of the
- /// Darknode Registry contract.
- /// @param _darknodeRegistry The address of the new Darknode Registry
- /// contract.
- function updateDarknodeRegistry(DarknodeRegistryLogicV1 _darknodeRegistry)
- external
- onlyOwner
- {
- _updateDarknodeRegistry(_darknodeRegistry);
- }
-
- /// @notice Allows the contract owner to update the address of the new dev
- /// fund.
- /// @param _communityFund The address of new community fund address.
- function updateCommunityFund(address _communityFund) external onlyOwner {
- _updateCommunityFund(_communityFund);
- }
-
- /// @notice Updates the proportion of the rewards that are withheld to be
- /// paid out over future cycles.
- ///
- /// @param _numerator The numerator of payout for darknodes.
- function updateHourlyPayoutWithheld(uint256 _numerator) external onlyOwner {
- require(
- _numerator <= HOURLY_PAYOUT_WITHHELD_DENOMINATOR,
- "ClaimlessRewards: invalid numerator"
- );
-
- // Emit before updating so that the old payout can be logged.
- emit LogHourlyPayoutChanged(_numerator, hourlyPayoutWithheldNumerator);
- hourlyPayoutWithheldNumerator = _numerator;
- }
-
- /// @notice Updates the proportion of the rewards that are withheld to be
- /// sent to the community fund.
- ///
- /// @param _numerator The numerator of payout for darknodes.
- function updateCommunityFundNumerator(uint256 _numerator)
- external
- onlyOwner
- {
- _updateCommunityFundNumerator(_numerator);
- }
-
- /// @notice Allows the contract owner to initiate an ownership transfer of
- /// the DarknodePaymentStore.
- ///
- /// @param _newOwner The address to transfer the ownership to.
- function transferStoreOwnership(ClaimlessRewardsAdminHandler _newOwner)
- external
- onlyOwner
- {
- store.transferOwnership(address(_newOwner));
- _newOwner.claimStoreOwnership();
- }
-
- /// @notice Claims ownership of the store passed in to the constructor.
- /// `transferStoreOwnership` must have previously been called when
- /// transferring from another DarknodePaymentStore.
- function claimStoreOwnership() external {
- store.claimOwnership();
- }
-
- /// @notice See `updateDarknodeRegistry`.
- function _updateDarknodeRegistry(DarknodeRegistryLogicV1 _darknodeRegistry)
- internal
- {
- require(
- address(_darknodeRegistry) != address(0x0),
- "ClaimlessRewards: invalid Darknode Registry address"
- );
-
- // Emit before updating so that the old registry can be logged.
- emit LogDarknodeRegistryUpdated(_darknodeRegistry, darknodeRegistry);
- darknodeRegistry = _darknodeRegistry;
- }
-
- /// @notice See `updateCommunityFund`.
- function _updateCommunityFund(address _communityFund) internal {
- // Ensure that the community fund is set properly, and that it's not the
- // POOLED_REWARDS address, which would allow the darknode rewards to
- // be withdrawn to the community fund.
- require(
- address(_communityFund) != address(0x0),
- "ClaimlessRewards: invalid community fund address"
- );
-
- // Ensure that the community fund isn't a registered node (this would
- // allow anyone to withdraw the node's legacy rewards to the node's
- // address - not a big issue, but disallowed nonetheless).
- require(
- darknodeRegistry.getDarknodeOperator(_communityFund) ==
- address(0x0),
- "ClaimlessRewards: community fund must not be a registered darknode"
- );
-
- // Emit before updating so that the old registry can be logged.
- emit LogCommunityFundUpdated(_communityFund, communityFund);
- communityFund = _communityFund;
- }
-
- /// @notice See `_updateCommunityFundNumerator`.
- function _updateCommunityFundNumerator(uint256 _numerator) internal {
- require(
- _numerator <= HOURLY_PAYOUT_WITHHELD_DENOMINATOR,
- "ClaimlessRewards: invalid numerator"
- );
-
- // Emit before updating so that the old payout can be logged.
- emit LogCommunityFundNumeratorChanged(
- _numerator,
- communityFundNumerator
- );
- communityFundNumerator = _numerator;
- }
-}
-
-contract ClaimlessRewardsCycleHandler is
- ClaimlessRewardsEvents,
- ClaimlessRewardsState
-{
- using SafeMath for uint256;
-
- /// @notice (external view) Return the length of the array of cycle
- /// timestamps. This makes it easier for clients to loop through them.
- function epochTimestampsLength() external view returns (uint256) {
- return epochTimestamps.length;
- }
-
- /// @notice (external view) Returns the full array of timestamps. If this
- /// grows too large to return, they should be fetched one-by-one or by
- /// fetching tx logs.
- function getEpochTimestamps() external view returns (uint256[] memory) {
- return epochTimestamps;
- }
-
- /// @notice Changes the current cycle.
- /// Callable by anyone.
- function changeCycle() external returns (uint256) {
- uint256 numerator = hourlyPayoutWithheldNumerator;
- uint256 denominator = HOURLY_PAYOUT_WITHHELD_DENOMINATOR;
-
- uint256 newCycleTimestamp;
- uint256 rewardsWithheldNumerator = denominator;
- uint256 cycleTimestamp = latestCycleTimestamp;
- uint256 currentCommunityFundNumerator = communityFundNumerator;
-
- for (
- uint256 hour = cycleTimestamp;
- hour <= block.timestamp;
- hour += 1 hours
- ) {
- rewardsWithheldNumerator = rewardsWithheldNumerator
- .mul(numerator)
- .div(denominator);
- newCycleTimestamp = hour;
- }
-
- // If the caller is the Darknode Registry, set the cycle timestamp to be
- // the current timestamp so that the cycle and epoch are in sync
- // (instead of rounding down by up to an hour). This ensures that any
- // newly registered darknodes don't lose the fees until the next cycle.
- // The difference in time won't get counted in the rewards paiout
- // schedule.
- // Also, if the caller is the darknode registry, use the number of
- // darknodes in the previous epoch instead of the new once, since the
- // implementation calls `changeCycle` at the end of the epoch update,
- // not the start.
- uint256 shareCount;
- if (msg.sender == address(darknodeRegistry)) {
- newCycleTimestamp = block.timestamp;
- shareCount = darknodeRegistry.numDarknodesPreviousEpoch();
- epochTimestamps.push(newCycleTimestamp);
- } else {
- shareCount = darknodeRegistry.numDarknodes();
- }
-
- // Require that at least an hour has passed since the last cycle, or,
- // if being called from the epoch function, that cycle wasn't called
- // previously in the same block.
- require(
- newCycleTimestamp > cycleTimestamp,
- "ClaimlessRewards: previous cycle too recent"
- );
-
- // Snapshot balances for the past cycle
- address nextToken = registeredTokens.begin();
- while (nextToken != address(0x0)) {
- {
- uint256 total = store.availableBalance(nextToken);
- uint256 totalWithheld =
- (total.mul(rewardsWithheldNumerator)).div(denominator);
-
- // The amount being paid out to the darknodes and the community
- // fund.
- uint256 totalPayout = total.sub(totalWithheld);
-
- // The amount being paid out to the community fund.
- uint256 communityFundPayout =
- totalPayout.mul(currentCommunityFundNumerator).div(
- denominator
- );
-
- // The amount being paid out to the darknodes.
- uint256 nodePayout = totalPayout.sub(communityFundPayout);
-
- // The amount being paid out to each indidivual darknode.
- uint256 share =
- shareCount == 0 ? 0 : nodePayout.div(shareCount);
-
- // The amount being paid out to the darknodes after ignoring
- // the amount left-over from dividing.
- uint256 nodePayoutAdjusted = share.mul(shareCount);
-
- // Store funds that can now be withdrawn by darknodes.
- if (nodePayoutAdjusted > 0) {
- store.incrementDarknodeBalance(
- POOLED_REWARDS,
- nextToken,
- nodePayoutAdjusted
- );
- }
-
- // Store funds that can be withdrawn to the community fund.
- if (communityFundPayout > 0) {
- store.incrementDarknodeBalance(
- communityFund,
- nextToken,
- communityFundPayout
- );
- }
-
- cycleCumulativeTokenShares[newCycleTimestamp][
- nextToken
- ] = cycleCumulativeTokenShares[cycleTimestamp][nextToken].add(
- share
- );
- }
-
- // Take next token.
- nextToken = registeredTokens.next(nextToken);
- }
-
- // Keep track of deregistered token amounts.
- address nextDeregisteredToken = deregisteredTokens.begin();
- while (nextDeregisteredToken != address(0x0)) {
- {
- cycleCumulativeTokenShares[newCycleTimestamp][
- nextDeregisteredToken
- ] = cycleCumulativeTokenShares[cycleTimestamp][
- nextDeregisteredToken
- ];
- }
-
- // Take next token.
- nextDeregisteredToken = deregisteredTokens.next(
- nextDeregisteredToken
- );
- }
-
- // Start a new cycle
- latestCycleTimestamp = newCycleTimestamp;
-
- emit LogCycleChanged(newCycleTimestamp, cycleTimestamp, shareCount);
-
- return newCycleTimestamp;
- }
-}
-
-contract ClaimlessRewardsWithdrawHandler is
- ClaimlessRewardsEvents,
- ClaimlessRewardsState,
- ClaimlessRewardsCycleHandler
-{
- using SafeMath for uint256;
- using SafeERC20 for ERC20;
-
- // Return the next cycle with a timestamp greater than or equal to the
- // passed in timestamp.
- function getNextEpochFromTimestamp(uint256 _target)
- public
- view
- returns (uint256)
- {
- uint256 start = 0;
- uint256 end = epochTimestamps.length.sub(1);
-
- // Binary search. Relies on `epochTimestamps` being sorted.
- while (start <= end) {
- // Check if the middle element satisfies the conditions.
- uint256 mid = (start + end) / 2;
- if (
- epochTimestamps[mid] >= _target &&
- (mid == 0 || epochTimestamps[mid - 1] < _target)
- ) {
- return epochTimestamps[mid];
- }
- // Restrict the search space.
- else if (epochTimestamps[mid] < _target) {
- start = mid + 1;
- } else {
- end = mid.sub(1);
- }
- }
- return 0;
- }
-
- function darknodeBalances(address _node, address _token)
- external
- view
- returns (uint256)
- {
- uint256 nodeRegistered = darknodeRegistry.darknodeRegisteredAt(_node);
-
- uint256 newWithdrawable = 0;
- if (nodeRegistered > 0) {
- (newWithdrawable, ) = _calculateNewWithdrawable(_node, _token);
- }
- uint256 legacyWithdrawable = store.darknodeBalances(_node, _token);
-
- return newWithdrawable.add(legacyWithdrawable);
- }
-
- /// @notice Withdraw the provided asset for each node in the list.
- function withdrawToken(address[] memory _nodes, address _token) public {
- uint256 withdrawTotal = 0;
-
- for (uint256 i = 0; i < _nodes.length; i++) {
- address _node = _nodes[i];
-
- // The Darknode Registry already prevents IDs from being 0x0, but a
- // user could attempt to register the communityFund address as a
- // darknode and then withdraw the pending community fund rewards.
- require(
- _node != POOLED_REWARDS && _node != communityFund,
- "ClaimlessRewards: invalid node ID"
- );
-
- require(
- darknodeRegistry.getDarknodeOperator(_node) == msg.sender,
- "ClaimlessRewards: not operator"
- );
-
- (uint256 newRewards, uint256 claimUntil) =
- _calculateNewWithdrawable(_node, _token);
-
- withdrawTotal = withdrawTotal.add(newRewards);
- rewardsLastClaimed[_node][_token] = claimUntil;
- emit LogDarknodeWithdrew(_node, newRewards, _token);
-
- // Check if there's a legacy amount to withdraw. This only has
- // to be withdrawn once.
- uint256 legacyAmount = store.darknodeBalances(_node, _token);
- if (legacyAmount > 0) {
- store.transfer(_node, _token, legacyAmount, msg.sender);
- emit LogDarknodeWithdrew(_node, legacyAmount, _token);
- }
- }
-
- store.transfer(POOLED_REWARDS, _token, withdrawTotal, msg.sender);
- }
-
- /// @notice Withdraw multiple assets for each darknode in the list.
- /// The interface has been kept the same as the DarknodePayment contract
- /// for backward-compatibility.
- function withdrawMultiple(address[] memory _nodes, address[] memory _tokens)
- public
- {
- for (uint256 i = 0; i < _tokens.length; i++) {
- withdrawToken(_nodes, _tokens[i]);
- }
- }
-
- /// @notice Withdraw the provided asset for the given darknode.
- /// The interface has been kept the same as the DarknodePayment contract
- /// for backward-compatibility.
- function withdraw(address _node, address _token) public {
- address[] memory nodes = new address[](1);
- nodes[0] = _node;
- return withdrawToken(nodes, _token);
- }
-
- function withdrawToCommunityFund(address[] memory _tokens) public {
- // Access storage outside of loop.
- address memoryCommunityFund = communityFund;
- address payable communityFundPayable =
- address(uint160(address(memoryCommunityFund)));
-
- for (uint256 i = 0; i < _tokens.length; i++) {
- address _token = _tokens[i];
- uint256 amount =
- store.darknodeBalances(memoryCommunityFund, _token);
-
- if (amount > 0) {
- store.transfer(
- memoryCommunityFund,
- _token,
- amount,
- communityFundPayable
- );
- }
- }
- }
-
- function _calculateNewWithdrawable(address _node, address _token)
- internal
- view
- returns (uint256, uint256)
- {
- uint256 nodeRegistered = darknodeRegistry.darknodeRegisteredAt(_node);
-
- require(nodeRegistered > 0, "ClaimlessRewards: not registered");
-
- uint256 nodeDeregistered =
- darknodeRegistry.darknodeDeregisteredAt(_node);
-
- uint256 claimFrom = rewardsLastClaimed[_node][_token];
- if (claimFrom < nodeRegistered) {
- claimFrom = getNextEpochFromTimestamp(nodeRegistered);
-
- // A node can start claiming from the first epoch after (or at) its
- // registration time. If this is 0, then the node is still in the
- // pending-registration state.
- require(claimFrom > 0, "ClaimlessRewards: registration pending");
- }
-
- uint256 claimUntil = latestCycleTimestamp;
- if (nodeDeregistered != 0) {
- uint256 deregisteredCycle =
- getNextEpochFromTimestamp(nodeDeregistered);
-
- // A node can only claim up until the next epoch after (or at) its
- // deregistration time. If this is 0, then the node is still
- // in the pending-deregistration state.
- if (deregisteredCycle != 0) {
- claimUntil = deregisteredCycle;
- }
- }
-
- uint256 lastCumulativeShare =
- cycleCumulativeTokenShares[claimFrom][_token];
- uint256 currentCumulativeShare =
- cycleCumulativeTokenShares[claimUntil][_token];
-
- return (
- currentCumulativeShare.sub(
- lastCumulativeShare,
- "ClaimlessRewards: error calculating withdrawable balance"
- ),
- claimUntil
- );
- }
-}
-
-/// @notice ClaimlessRewards is intended to replace the DarknodePayment
-/// contract. It's to main improvements are:
-/// 1) no longer requiring nodes to call `claim` each epoch, and
-/// 2) allowing for a community fund to earn a proportion of the rewards.
-contract ClaimlessRewards is
- Claimable,
- ClaimlessRewardsEvents,
- ClaimlessRewardsState,
- ClaimlessRewardsTokenHandler,
- ClaimlessRewardsAdminHandler,
- ClaimlessRewardsCycleHandler,
- ClaimlessRewardsWithdrawHandler
-{
- /// @notice The contract constructor. Starts the current cycle using the
- /// latest epoch.
- ///
- /// @dev The DarknodeRegistry should be set to point to the
- /// ClaimlessRewards contract before the next epoch is called.
- ///
- /// @param _darknodeRegistry The address of the DarknodeRegistry contract
- /// @param _darknodePaymentStore The address of the DarknodePaymentStore
- /// contract. Can be updated by the contract owner.
- /// @param _communityFund The address to which the community fund balances
- /// can be withdrawn to. Can be updated by the contract owner.
- /// @param _communityFundNumerator The portion of the rewards that are paid
- /// to the community fund. Can be updated by the contract owner.
- constructor(
- DarknodeRegistryLogicV1 _darknodeRegistry,
- DarknodePaymentStore _darknodePaymentStore,
- address _communityFund,
- uint256 _communityFundNumerator
- ) public Claimable() {
- Claimable.initialize(msg.sender);
-
- store = _darknodePaymentStore;
- _updateDarknodeRegistry(_darknodeRegistry);
- _updateCommunityFund(_communityFund);
- _updateCommunityFundNumerator(_communityFundNumerator);
-
- // Initialize the current cycle to the start of the Registry's epoch.
- (, latestCycleTimestamp) = darknodeRegistry.currentEpoch();
- epochTimestamps.push(latestCycleTimestamp);
- }
-}
diff --git a/contracts/DarknodePayment/DarknodePayment.sol b/contracts/DarknodePayment/DarknodePayment.sol
deleted file mode 100644
index 3ce105df..00000000
--- a/contracts/DarknodePayment/DarknodePayment.sol
+++ /dev/null
@@ -1,521 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-
-import "../libraries/ERC20WithFees.sol";
-import "../DarknodeRegistry/DarknodeRegistry.sol";
-import "./DarknodePaymentStore.sol";
-
-/// @notice DarknodePayment is responsible for paying off darknodes for their
-/// computation.
-contract DarknodePayment is Claimable {
- using SafeMath for uint256;
- using SafeERC20 for ERC20;
- using ERC20WithFees for ERC20;
-
- string public VERSION; // Passed in as a constructor parameter.
-
- /// @notice The special address for Ether.
- address public constant ETHEREUM =
- 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
-
- DarknodeRegistryLogicV1 public darknodeRegistry; // Passed in as a constructor parameter.
-
- /// @notice DarknodePaymentStore is the storage contract for darknode
- /// payments.
- DarknodePaymentStore public store; // Passed in as a constructor parameter.
-
- /// @notice The address that can call changeCycle()
- // This defaults to the owner but should be changed to the DarknodeRegistry.
- address public cycleChanger;
-
- uint256 public currentCycle;
- uint256 public previousCycle;
-
- /// @notice The list of tokens that will be registered next cycle.
- /// We only update the shareCount at the change of cycle to
- /// prevent the number of shares from changing.
- address[] public pendingTokens;
-
- /// @notice The list of tokens which are already registered and rewards can
- /// be claimed for.
- address[] public registeredTokens;
-
- /// @notice Mapping from token -> index. Index starts from 1. 0 means not in
- /// list.
- mapping(address => uint256) public registeredTokenIndex;
-
- /// @notice Mapping from token -> amount.
- /// The amount of rewards allocated for all darknodes to claim into
- /// their account.
- mapping(address => uint256) public unclaimedRewards;
-
- /// @notice Mapping from token -> amount.
- /// The amount of rewards allocated for each darknode.
- mapping(address => uint256) public previousCycleRewardShare;
-
- /// @notice The time that the current cycle started.
- uint256 public cycleStartTime;
-
- /// @notice The staged payout percentage to the darknodes per cycle.
- uint256 public nextCyclePayoutPercent;
-
- /// @notice The current cycle payout percentage to the darknodes.
- uint256 public currentCyclePayoutPercent;
-
- /// @notice Mapping of darknode -> cycle -> already_claimed.
- /// Used to keep track of which darknodes have already claimed their
- /// rewards.
- mapping(address => mapping(uint256 => bool)) public rewardClaimed;
-
- /// @notice Emitted when a darknode claims their share of reward.
- /// @param _darknode The darknode which claimed.
- /// @param _cycle The cycle that the darknode claimed for.
- event LogDarknodeClaim(address indexed _darknode, uint256 _cycle);
-
- /// @notice Emitted when someone pays the DarknodePayment contract.
- /// @param _payer The darknode which claimed.
- /// @param _amount The cycle that the darknode claimed for.
- /// @param _token The address of the token that was transferred.
- event LogPaymentReceived(
- address indexed _payer,
- address indexed _token,
- uint256 _amount
- );
-
- /// @notice Emitted when a darknode calls withdraw.
- /// @param _darknodeOperator The address of the darknode's operator.
- /// @param _darknodeID The address of the darknode which withdrew.
- /// @param _value The amount of DAI withdrawn.
- /// @param _token The address of the token that was withdrawn.
- event LogDarknodeWithdrew(
- address indexed _darknodeOperator,
- address indexed _darknodeID,
- address indexed _token,
- uint256 _value
- );
-
- /// @notice Emitted when the payout percent changes.
- /// @param _newPercent The new percent.
- /// @param _oldPercent The old percent.
- event LogPayoutPercentChanged(uint256 _newPercent, uint256 _oldPercent);
-
- /// @notice Emitted when the CycleChanger address changes.
- /// @param _newCycleChanger The new CycleChanger.
- /// @param _oldCycleChanger The old CycleChanger.
- event LogCycleChangerChanged(
- address indexed _newCycleChanger,
- address indexed _oldCycleChanger
- );
-
- /// @notice Emitted when a new token is registered.
- /// @param _token The token that was registered.
- event LogTokenRegistered(address indexed _token);
-
- /// @notice Emitted when a token is deregistered.
- /// @param _token The token that was deregistered.
- event LogTokenDeregistered(address indexed _token);
-
- /// @notice Emitted when the DarknodeRegistry is updated.
- /// @param _previousDarknodeRegistry The address of the old registry.
- /// @param _nextDarknodeRegistry The address of the new registry.
- event LogDarknodeRegistryUpdated(
- DarknodeRegistryLogicV1 indexed _previousDarknodeRegistry,
- DarknodeRegistryLogicV1 indexed _nextDarknodeRegistry
- );
-
- /// @notice Restrict a function registered dark nodes to call a function.
- modifier onlyDarknode(address _darknode) {
- require(
- darknodeRegistry.isRegistered(_darknode),
- "DarknodePayment: darknode is not registered"
- );
- _;
- }
-
- /// @notice Restrict a function to have a valid percentage.
- modifier validPercent(uint256 _percent) {
- require(_percent <= 100, "DarknodePayment: invalid percentage");
- _;
- }
-
- /// @notice Restrict a function to be called by cycleChanger.
- modifier onlyCycleChanger {
- require(
- msg.sender == cycleChanger,
- "DarknodePayment: not cycle changer"
- );
- _;
- }
-
- /// @notice The contract constructor. Starts the current cycle using the
- /// time of deploy.
- ///
- /// @param _VERSION A string defining the contract version.
- /// @param _darknodeRegistry The address of the DarknodeRegistry contract.
- /// @param _darknodePaymentStore The address of the DarknodePaymentStore
- /// contract.
- constructor(
- string memory _VERSION,
- DarknodeRegistryLogicV1 _darknodeRegistry,
- DarknodePaymentStore _darknodePaymentStore,
- uint256 _cyclePayoutPercent
- ) public validPercent(_cyclePayoutPercent) {
- Claimable.initialize(msg.sender);
- VERSION = _VERSION;
- darknodeRegistry = _darknodeRegistry;
- store = _darknodePaymentStore;
- nextCyclePayoutPercent = _cyclePayoutPercent;
- // Default the cycleChanger to owner.
- cycleChanger = msg.sender;
-
- // Start the current cycle
- (currentCycle, cycleStartTime) = darknodeRegistry.currentEpoch();
- currentCyclePayoutPercent = nextCyclePayoutPercent;
- }
-
- /// @notice Allows the contract owner to update the address of the
- /// darknode registry contract.
- /// @param _darknodeRegistry The address of the Darknode Registry
- /// contract.
- function updateDarknodeRegistry(DarknodeRegistryLogicV1 _darknodeRegistry)
- external
- onlyOwner
- {
- require(
- address(_darknodeRegistry) != address(0x0),
- "DarknodePayment: invalid Darknode Registry address"
- );
- DarknodeRegistryLogicV1 previousDarknodeRegistry = darknodeRegistry;
- darknodeRegistry = _darknodeRegistry;
- emit LogDarknodeRegistryUpdated(
- previousDarknodeRegistry,
- darknodeRegistry
- );
- }
-
- /// @notice Transfers the funds allocated to the darknode to the darknode
- /// owner.
- ///
- /// @param _darknode The address of the darknode.
- /// @param _token Which token to transfer.
- function withdraw(address _darknode, address _token) public {
- address payable darknodeOperator =
- darknodeRegistry.getDarknodeOperator(_darknode);
- require(
- darknodeOperator != address(0x0),
- "DarknodePayment: invalid darknode owner"
- );
-
- uint256 amount = store.darknodeBalances(_darknode, _token);
-
- // Skip if amount is zero.
- if (amount > 0) {
- store.transfer(_darknode, _token, amount, darknodeOperator);
- emit LogDarknodeWithdrew(
- darknodeOperator,
- _darknode,
- _token,
- amount
- );
- }
- }
-
- function withdrawMultiple(
- address[] calldata _darknodes,
- address[] calldata _tokens
- ) external {
- for (uint256 i = 0; i < _darknodes.length; i++) {
- for (uint256 j = 0; j < _tokens.length; j++) {
- withdraw(_darknodes[i], _tokens[j]);
- }
- }
- }
-
- /// @notice Forward all payments to the DarknodePaymentStore.
- function() external payable {
- address(store).transfer(msg.value);
- emit LogPaymentReceived(msg.sender, ETHEREUM, msg.value);
- }
-
- /// @notice The current balance of the contract available as reward for the
- /// current cycle.
- function currentCycleRewardPool(address _token)
- external
- view
- returns (uint256)
- {
- uint256 total =
- store.availableBalance(_token).sub(
- unclaimedRewards[_token],
- "DarknodePayment: unclaimed rewards exceed total rewards"
- );
- return total.div(100).mul(currentCyclePayoutPercent);
- }
-
- function darknodeBalances(address _darknodeID, address _token)
- external
- view
- returns (uint256)
- {
- return store.darknodeBalances(_darknodeID, _token);
- }
-
- /// @notice Changes the current cycle.
- function changeCycle() external onlyCycleChanger returns (uint256) {
- // Snapshot balances for the past cycle.
- uint256 arrayLength = registeredTokens.length;
- for (uint256 i = 0; i < arrayLength; i++) {
- _snapshotBalance(registeredTokens[i]);
- }
-
- // Start a new cycle.
- previousCycle = currentCycle;
- (currentCycle, cycleStartTime) = darknodeRegistry.currentEpoch();
- currentCyclePayoutPercent = nextCyclePayoutPercent;
-
- // Update the list of registeredTokens.
- _updateTokenList();
- return currentCycle;
- }
-
- /// @notice Deposits token into the contract to be paid to the Darknodes.
- ///
- /// @param _value The amount of token deposit in the token's smallest unit.
- /// @param _token The token address.
- function deposit(uint256 _value, address _token) external payable {
- uint256 receivedValue;
- if (_token == ETHEREUM) {
- require(
- _value == msg.value,
- "DarknodePayment: mismatched deposit value"
- );
- receivedValue = msg.value;
- address(store).transfer(msg.value);
- } else {
- require(
- msg.value == 0,
- "DarknodePayment: unexpected ether transfer"
- );
- require(
- registeredTokenIndex[_token] != 0,
- "DarknodePayment: token not registered"
- );
- // Forward the funds to the store.
- receivedValue = ERC20(_token).safeTransferFromWithFees(
- msg.sender,
- address(store),
- _value
- );
- }
- emit LogPaymentReceived(msg.sender, _token, receivedValue);
- }
-
- /// @notice Forwards any tokens that have been sent to the DarknodePayment contract
- /// probably by mistake, to the DarknodePaymentStore.
- ///
- /// @param _token The token address.
- function forward(address _token) external {
- if (_token == ETHEREUM) {
- // Its unlikely that ETH will need to be forwarded, but it is
- // possible. For example - if ETH had already been sent to the
- // contract's address before it was deployed, or if funds are sent
- // to it as part of a contract's self-destruct.
- address(store).transfer(address(this).balance);
- } else {
- ERC20(_token).safeTransfer(
- address(store),
- ERC20(_token).balanceOf(address(this))
- );
- }
- }
-
- /// @notice Claims the rewards allocated to the darknode last epoch.
- /// @param _darknode The address of the darknode to claim.
- function claim(address _darknode) external onlyDarknode(_darknode) {
- require(
- darknodeRegistry.isRegisteredInPreviousEpoch(_darknode),
- "DarknodePayment: cannot claim for this epoch"
- );
- // Claim share of rewards allocated for last cycle.
- _claimDarknodeReward(_darknode);
- emit LogDarknodeClaim(_darknode, previousCycle);
- }
-
- /// @notice Adds tokens to be payable. Registration is pending until next
- /// cycle.
- ///
- /// @param _token The address of the token to be registered.
- function registerToken(address _token) external onlyOwner {
- require(
- registeredTokenIndex[_token] == 0,
- "DarknodePayment: token already registered"
- );
- require(
- !tokenPendingRegistration(_token),
- "DarknodePayment: token already pending registration"
- );
- pendingTokens.push(_token);
- }
-
- function tokenPendingRegistration(address _token)
- public
- view
- returns (bool)
- {
- uint256 arrayLength = pendingTokens.length;
- for (uint256 i = 0; i < arrayLength; i++) {
- if (pendingTokens[i] == _token) {
- return true;
- }
- }
- return false;
- }
-
- /// @notice Removes a token from the list of supported tokens.
- /// Deregistration is pending until next cycle.
- ///
- /// @param _token The address of the token to be deregistered.
- function deregisterToken(address _token) external onlyOwner {
- require(
- registeredTokenIndex[_token] > 0,
- "DarknodePayment: token not registered"
- );
- _deregisterToken(_token);
- }
-
- /// @notice Updates the CycleChanger contract address.
- ///
- /// @param _addr The new CycleChanger contract address.
- function updateCycleChanger(address _addr) external onlyOwner {
- require(
- _addr != address(0),
- "DarknodePayment: invalid contract address"
- );
- emit LogCycleChangerChanged(_addr, cycleChanger);
- cycleChanger = _addr;
- }
-
- /// @notice Updates payout percentage.
- ///
- /// @param _percent The percentage of payout for darknodes.
- function updatePayoutPercentage(uint256 _percent)
- external
- onlyOwner
- validPercent(_percent)
- {
- uint256 oldPayoutPercent = nextCyclePayoutPercent;
- nextCyclePayoutPercent = _percent;
- emit LogPayoutPercentChanged(nextCyclePayoutPercent, oldPayoutPercent);
- }
-
- /// @notice Allows the contract owner to initiate an ownership transfer of
- /// the DarknodePaymentStore.
- ///
- /// @param _newOwner The address to transfer the ownership to.
- function transferStoreOwnership(DarknodePayment _newOwner)
- external
- onlyOwner
- {
- store.transferOwnership(address(_newOwner));
- _newOwner.claimStoreOwnership();
- }
-
- /// @notice Claims ownership of the store passed in to the constructor.
- /// `transferStoreOwnership` must have previously been called when
- /// transferring from another DarknodePaymentStore.
- function claimStoreOwnership() external {
- store.claimOwnership();
- }
-
- /// @notice Claims the darknode reward for all registered tokens into
- /// darknodeBalances in the DarknodePaymentStore.
- /// Rewards can only be claimed once per cycle.
- ///
- /// @param _darknode The address to the darknode to claim rewards for.
- function _claimDarknodeReward(address _darknode) private {
- require(
- !rewardClaimed[_darknode][previousCycle],
- "DarknodePayment: reward already claimed"
- );
- rewardClaimed[_darknode][previousCycle] = true;
- uint256 arrayLength = registeredTokens.length;
- for (uint256 i = 0; i < arrayLength; i++) {
- address token = registeredTokens[i];
-
- // Only increment balance if shares were allocated last cycle
- if (previousCycleRewardShare[token] > 0) {
- unclaimedRewards[token] = unclaimedRewards[token].sub(
- previousCycleRewardShare[token],
- "DarknodePayment: share exceeds unclaimed rewards"
- );
- store.incrementDarknodeBalance(
- _darknode,
- token,
- previousCycleRewardShare[token]
- );
- }
- }
- }
-
- /// @notice Snapshots the current balance of the tokens, for all registered
- /// tokens.
- ///
- /// @param _token The address the token to snapshot.
- function _snapshotBalance(address _token) private {
- uint256 shareCount = darknodeRegistry.numDarknodesPreviousEpoch();
- if (shareCount == 0) {
- unclaimedRewards[_token] = 0;
- previousCycleRewardShare[_token] = 0;
- } else {
- // Lock up the current balance for darknode reward allocation
- uint256 total = store.availableBalance(_token);
- unclaimedRewards[_token] = total.div(100).mul(
- currentCyclePayoutPercent
- );
- previousCycleRewardShare[_token] = unclaimedRewards[_token].div(
- shareCount
- );
- }
- }
-
- /// @notice Deregisters a token, removing it from the list of
- /// registeredTokens.
- ///
- /// @param _token The address of the token to deregister.
- function _deregisterToken(address _token) private {
- address lastToken =
- registeredTokens[
- registeredTokens.length.sub(
- 1,
- "DarknodePayment: no tokens registered"
- )
- ];
- uint256 deletedTokenIndex = registeredTokenIndex[_token].sub(1);
- // Move the last token to _token's position and update it's index
- registeredTokens[deletedTokenIndex] = lastToken;
- registeredTokenIndex[lastToken] = registeredTokenIndex[_token];
- // Decreasing the length will clean up the storage for us
- // So we don't need to manually delete the element
- registeredTokens.pop();
- registeredTokenIndex[_token] = 0;
-
- emit LogTokenDeregistered(_token);
- }
-
- /// @notice Updates the list of registeredTokens adding tokens that are to be registered.
- /// The list of tokens that are pending registration are emptied afterwards.
- function _updateTokenList() private {
- // Register tokens
- uint256 arrayLength = pendingTokens.length;
- for (uint256 i = 0; i < arrayLength; i++) {
- address token = pendingTokens[i];
- registeredTokens.push(token);
- registeredTokenIndex[token] = registeredTokens.length;
- emit LogTokenRegistered(token);
- }
- pendingTokens.length = 0;
- }
-}
diff --git a/contracts/DarknodePayment/DarknodePaymentMigrator.sol b/contracts/DarknodePayment/DarknodePaymentMigrator.sol
deleted file mode 100644
index 68e63e90..00000000
--- a/contracts/DarknodePayment/DarknodePaymentMigrator.sol
+++ /dev/null
@@ -1,69 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-
-import "../Governance/Claimable.sol";
-import "./DarknodePaymentStore.sol";
-import "./DarknodePayment.sol";
-
-/// @notice The DarknodePaymentMigrator migrates unclaimed funds from the
-/// DarknodePayment contract. In a single transaction, it claims the store
-/// ownership from the DNP contract, migrates unclaimed fees and then returns
-/// the store ownership back to the DNP.
-contract DarknodePaymentMigrator is Claimable {
- DarknodePayment public dnp;
- address[] public tokens;
-
- constructor(DarknodePayment _dnp, address[] memory _tokens) public {
- Claimable.initialize(msg.sender);
- dnp = _dnp;
- tokens = _tokens;
- }
-
- function setTokens(address[] memory _tokens) public onlyOwner {
- tokens = _tokens;
- }
-
- function claimStoreOwnership() external {
- require(msg.sender == address(dnp), "Not darknode payment contract");
- DarknodePaymentStore store = dnp.store();
-
- store.claimOwnership();
-
- for (uint256 i = 0; i < tokens.length; i++) {
- address token = tokens[i];
-
- uint256 unclaimed = store.availableBalance(token);
-
- if (unclaimed > 0) {
- store.incrementDarknodeBalance(address(0x0), token, unclaimed);
-
- store.transfer(
- address(0x0),
- token,
- unclaimed,
- _payableAddress(owner())
- );
- }
- }
-
- store.transferOwnership(address(dnp));
- dnp.claimStoreOwnership();
-
- require(
- store.owner() == address(dnp),
- "Store ownership not transferred back."
- );
- }
-
- // Cast an address to a payable address
- function _payableAddress(address a)
- internal
- pure
- returns (address payable)
- {
- return address(uint160(address(a)));
- }
-}
diff --git a/contracts/DarknodePayment/DarknodePaymentStore.sol b/contracts/DarknodePayment/DarknodePaymentStore.sol
deleted file mode 100644
index c54b1e8e..00000000
--- a/contracts/DarknodePayment/DarknodePaymentStore.sol
+++ /dev/null
@@ -1,125 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-
-import "../Governance/Claimable.sol";
-import "../libraries/ERC20WithFees.sol";
-
-/// @notice DarknodePaymentStore is responsible for tracking balances which have
-/// been allocated to the darknodes. It is also responsible for holding
-/// the tokens to be paid out to darknodes.
-contract DarknodePaymentStore is Claimable {
- using SafeMath for uint256;
- using SafeERC20 for ERC20;
- using ERC20WithFees for ERC20;
-
- string public VERSION; // Passed in as a constructor parameter.
-
- /// @notice The special address for Ether.
- address public constant ETHEREUM =
- 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
-
- /// @notice Mapping of darknode -> token -> balance.
- mapping(address => mapping(address => uint256)) public darknodeBalances;
-
- /// @notice Mapping of token -> lockedAmount.
- mapping(address => uint256) public lockedBalances;
-
- /// @notice The contract constructor.
- ///
- /// @param _VERSION A string defining the contract version.
- constructor(string memory _VERSION) public {
- Claimable.initialize(msg.sender);
- VERSION = _VERSION;
- }
-
- /// @notice Allow direct ETH payments to be made to the DarknodePaymentStore.
- function() external payable {}
-
- /// @notice Get the total balance of the contract for a particular token.
- ///
- /// @param _token The token to check balance of.
- /// @return The total balance of the contract.
- function totalBalance(address _token) public view returns (uint256) {
- if (_token == ETHEREUM) {
- return address(this).balance;
- } else {
- return ERC20(_token).balanceOf(address(this));
- }
- }
-
- /// @notice Get the available balance of the contract for a particular token
- /// This is the free amount which has not yet been allocated to
- /// darknodes.
- ///
- /// @param _token The token to check balance of.
- /// @return The available balance of the contract.
- function availableBalance(address _token) public view returns (uint256) {
- return
- totalBalance(_token).sub(
- lockedBalances[_token],
- "DarknodePaymentStore: locked balance exceed total balance"
- );
- }
-
- /// @notice Increments the amount of funds allocated to a particular
- /// darknode.
- ///
- /// @param _darknode The address of the darknode to increase balance of.
- /// @param _token The token which the balance should be incremented.
- /// @param _amount The amount that the balance should be incremented by.
- function incrementDarknodeBalance(
- address _darknode,
- address _token,
- uint256 _amount
- ) external onlyOwner {
- require(_amount > 0, "DarknodePaymentStore: invalid amount");
- require(
- availableBalance(_token) >= _amount,
- "DarknodePaymentStore: insufficient contract balance"
- );
-
- darknodeBalances[_darknode][_token] = darknodeBalances[_darknode][
- _token
- ]
- .add(_amount);
- lockedBalances[_token] = lockedBalances[_token].add(_amount);
- }
-
- /// @notice Transfers an amount out of balance to a specified address.
- ///
- /// @param _darknode The address of the darknode.
- /// @param _token Which token to transfer.
- /// @param _amount The amount to transfer.
- /// @param _recipient The address to withdraw it to.
- function transfer(
- address _darknode,
- address _token,
- uint256 _amount,
- address payable _recipient
- ) external onlyOwner {
- require(
- darknodeBalances[_darknode][_token] >= _amount,
- "DarknodePaymentStore: insufficient darknode balance"
- );
- darknodeBalances[_darknode][_token] = darknodeBalances[_darknode][
- _token
- ]
- .sub(
- _amount,
- "DarknodePaymentStore: insufficient darknode balance for transfer"
- );
- lockedBalances[_token] = lockedBalances[_token].sub(
- _amount,
- "DarknodePaymentStore: insufficient token balance for transfer"
- );
-
- if (_token == ETHEREUM) {
- _recipient.transfer(_amount);
- } else {
- ERC20(_token).safeTransfer(_recipient, _amount);
- }
- }
-}
diff --git a/contracts/DarknodePayment/DarknodeRegistryForwarder.sol b/contracts/DarknodePayment/DarknodeRegistryForwarder.sol
deleted file mode 100644
index 55af44cf..00000000
--- a/contracts/DarknodePayment/DarknodeRegistryForwarder.sol
+++ /dev/null
@@ -1,45 +0,0 @@
-pragma solidity 0.5.17;
-
-import "../DarknodeRegistry/DarknodeRegistry.sol";
-
-/// @notice DarknodeRegistryForwarder implements the DNR's methods that are used
-/// by the DNP, and it forwards them all to the DNR except
-/// `isRegisteredInPreviousEpoch`, for which it returns false in order to make
-/// calls to `claim` revert.
-contract DarknodeRegistryForwarder {
- DarknodeRegistryLogicV1 dnr;
-
- constructor(DarknodeRegistryLogicV1 _dnr) public {
- dnr = _dnr;
- }
-
- /// @notice Returns if a darknode is in the registered state.
- function isRegistered(address _darknodeID) public view returns (bool) {
- return dnr.isRegistered(_darknodeID);
- }
-
- function currentEpoch() public view returns (uint256, uint256) {
- return dnr.currentEpoch();
- }
-
- function getDarknodeOperator(address _darknodeID)
- public
- view
- returns (address payable)
- {
- return dnr.getDarknodeOperator(_darknodeID);
- }
-
- function isRegisteredInPreviousEpoch(address _darknodeID)
- public
- view
- returns (bool)
- {
- // return dnr.isRegisteredInPreviousEpoch(_darknodeID);
- return false;
- }
-
- function numDarknodesPreviousEpoch() public view returns (uint256) {
- return dnr.numDarknodesPreviousEpoch();
- }
-}
diff --git a/contracts/DarknodePayment/ValidString.sol b/contracts/DarknodePayment/ValidString.sol
deleted file mode 100644
index 2cbc0fe8..00000000
--- a/contracts/DarknodePayment/ValidString.sol
+++ /dev/null
@@ -1,27 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-pragma solidity 0.5.17;
-
-library ValidString {
- function isAlphanumeric(string memory _string)
- internal
- pure
- returns (bool)
- {
- for (uint256 i = 0; i < bytes(_string).length; i++) {
- uint8 char = uint8(bytes(_string)[i]);
- if (
- !((char >= 65 && char <= 90) ||
- (char >= 97 && char <= 122) ||
- (char >= 48 && char <= 57))
- ) {
- return false;
- }
- }
- return true;
- }
-
- function isNotEmpty(string memory _string) internal pure returns (bool) {
- return bytes(_string).length > 0;
- }
-}
diff --git a/contracts/DarknodeRegistry/DarknodeRegistry.sol b/contracts/DarknodeRegistry/DarknodeRegistry.sol
index 35898db9..142f023a 100644
--- a/contracts/DarknodeRegistry/DarknodeRegistry.sol
+++ b/contracts/DarknodeRegistry/DarknodeRegistry.sol
@@ -1,6 +1,7 @@
pragma solidity 0.5.17;
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
+import "@openzeppelin/contracts-ethereum-package/contracts/cryptography/ECDSA.sol";
import "@openzeppelin/upgrades/contracts/upgradeability/InitializableAdminUpgradeabilityProxy.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";
@@ -8,72 +9,20 @@ import "../RenToken/RenToken.sol";
import "./DarknodeRegistryStore.sol";
import "../Governance/Claimable.sol";
import "../libraries/CanReclaimTokens.sol";
+import "./DarknodeRegistryV1.sol";
-interface IDarknodePaymentStore {}
-
-interface IDarknodePayment {
- function changeCycle() external returns (uint256);
-
- function store() external view returns (IDarknodePaymentStore);
-}
-
-interface IDarknodeSlasher {}
-
-contract DarknodeRegistryStateV1 {
- using SafeMath for uint256;
-
- string public VERSION; // Passed in as a constructor parameter.
-
- /// @notice Darknode pods are shuffled after a fixed number of blocks.
- /// An Epoch stores an epoch hash used as an (insecure) RNG seed, and the
- /// blocknumber which restricts when the next epoch can be called.
- struct Epoch {
- uint256 epochhash;
- uint256 blocktime;
- }
-
- uint256 public numDarknodes;
- uint256 public numDarknodesNextEpoch;
- uint256 public numDarknodesPreviousEpoch;
-
- /// Variables used to parameterize behavior.
- uint256 public minimumBond;
- uint256 public minimumPodSize;
- uint256 public minimumEpochInterval;
- uint256 public deregistrationInterval;
-
- /// When one of the above variables is modified, it is only updated when the
- /// next epoch is called. These variables store the values for the next
- /// epoch.
- uint256 public nextMinimumBond;
- uint256 public nextMinimumPodSize;
- uint256 public nextMinimumEpochInterval;
-
- /// The current and previous epoch.
- Epoch public currentEpoch;
- Epoch public previousEpoch;
-
- /// REN ERC20 contract used to transfer bonds.
- RenToken public ren;
-
- /// Darknode Registry Store is the storage contract for darknodes.
- DarknodeRegistryStore public store;
-
- /// The Darknode Payment contract for changing cycle.
- IDarknodePayment public darknodePayment;
-
- /// Darknode Slasher allows darknodes to vote on bond slashing.
- IDarknodeSlasher public slasher;
- IDarknodeSlasher public nextSlasher;
-}
+contract DarknodeRegistryStateV2 {}
/// @notice DarknodeRegistry is responsible for the registration and
/// deregistration of Darknodes.
-contract DarknodeRegistryLogicV1 is
+contract DarknodeRegistryLogicV2 is
Claimable,
CanReclaimTokens,
- DarknodeRegistryStateV1
+ DarknodeRegistryStateV1,
+ DarknodeRegistryStateV2
{
+ using SafeMath for uint256;
+
/// @notice Emitted when a darknode is registered.
/// @param _darknodeOperator The owner of the darknode.
/// @param _darknodeID The ID of the darknode that was registered.
@@ -94,6 +43,7 @@ contract DarknodeRegistryLogicV1 is
/// @notice Emitted when a refund has been made.
/// @param _darknodeOperator The owner of the darknode.
+ /// @param _darknodeID The ID of the darknode that was refunded.
/// @param _amount The amount of REN that was refunded.
event LogDarknodeRefunded(
address indexed _darknodeOperator,
@@ -101,6 +51,18 @@ contract DarknodeRegistryLogicV1 is
uint256 _amount
);
+ /// @notice Emitted when a recovery has been made.
+ /// @param _darknodeOperator The owner of the darknode.
+ /// @param _darknodeID The ID of the darknode that was recovered.
+ /// @param _bondRecipient The address that received the bond.
+ /// @param _submitter The address that called the recover method.
+ event LogDarknodeRecovered(
+ address indexed _darknodeOperator,
+ address indexed _darknodeID,
+ address _bondRecipient,
+ address indexed _submitter
+ );
+
/// @notice Emitted when a darknode's bond is slashed.
/// @param _darknodeOperator The owner of the darknode.
/// @param _darknodeID The ID of the darknode that was slashed.
@@ -134,8 +96,8 @@ contract DarknodeRegistryLogicV1 is
address indexed _nextSlasher
);
event LogDarknodePaymentUpdated(
- IDarknodePayment indexed _previousDarknodePayment,
- IDarknodePayment indexed _nextDarknodePayment
+ address indexed _previousDarknodePayment,
+ address indexed _nextDarknodePayment
);
/// @notice Restrict a function to the owner that registered the darknode.
@@ -184,11 +146,10 @@ contract DarknodeRegistryLogicV1 is
_;
}
- /// @notice Restrict a function to registered nodes without a pending
- /// deregistration.
+ /// @notice Restrict a function to registered and deregistered nodes.
modifier onlyDarknode(address _darknodeID) {
require(
- isRegistered(_darknodeID),
+ isRegistered(_darknodeID) || isDeregistered(_darknodeID),
"DarknodeRegistry: invalid darknode"
);
_;
@@ -245,10 +206,8 @@ contract DarknodeRegistryLogicV1 is
/// caller of this method will be stored as the owner of the darknode.
///
/// @param _darknodeID The darknode ID that will be registered.
- /// @param _publicKey The public key of the darknode. It is stored to allow
- /// other darknodes and traders to encrypt messages to the trader.
- function register(address _darknodeID, bytes calldata _publicKey)
- external
+ function registerNode(address _darknodeID)
+ public
onlyRefunded(_darknodeID)
{
require(
@@ -267,7 +226,7 @@ contract DarknodeRegistryLogicV1 is
_darknodeID,
msg.sender,
minimumBond,
- _publicKey,
+ "",
currentEpoch.blocktime.add(minimumEpochInterval),
0
);
@@ -278,6 +237,70 @@ contract DarknodeRegistryLogicV1 is
emit LogDarknodeRegistered(msg.sender, _darknodeID, minimumBond);
}
+ /// @notice An alias for `registerNode` that includes the legacy public key
+ /// parameter.
+ /// @param _darknodeID The darknode ID that will be registered.
+ /// @param _publicKey Deprecated parameter - see `registerNode`.
+ function register(address _darknodeID, bytes calldata _publicKey) external {
+ return registerNode(_darknodeID);
+ }
+
+ /// @notice Register multiple darknodes and transfer the bonds to this contract.
+ /// Before registering, the bonds transfer must be approved in the REN contract.
+ /// The darknodes will remain pending registration until the next epoch. Only
+ /// after this period can the darknodes be deregistered. The caller of this method
+ /// will be stored as the owner of each darknode. If one registration fails, all
+ /// registrations fail.
+ /// @param _darknodeIDs The darknode IDs that will be registered.
+ function registerMultiple(address[] calldata _darknodeIDs) external {
+ // Save variables in memory to prevent redundant reads from storage
+ DarknodeRegistryStore _store = store;
+ Epoch memory _currentEpoch = currentEpoch;
+ uint256 nextRegisteredAt = _currentEpoch.blocktime.add(
+ minimumEpochInterval
+ );
+ uint256 _minimumBond = minimumBond;
+
+ require(
+ ren.transferFrom(
+ msg.sender,
+ address(_store),
+ _minimumBond.mul(_darknodeIDs.length)
+ ),
+ "DarknodeRegistry: bond transfers failed"
+ );
+
+ for (uint256 i = 0; i < _darknodeIDs.length; i++) {
+ address darknodeID = _darknodeIDs[i];
+
+ uint256 registeredAt = _store.darknodeRegisteredAt(darknodeID);
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(darknodeID);
+
+ require(
+ _isRefunded(registeredAt, deregisteredAt),
+ "DarknodeRegistry: must be refunded or never registered"
+ );
+
+ require(
+ darknodeID != address(0),
+ "DarknodeRegistry: darknode address cannot be zero"
+ );
+
+ _store.appendDarknode(
+ darknodeID,
+ msg.sender,
+ _minimumBond,
+ "",
+ nextRegisteredAt,
+ 0
+ );
+
+ emit LogDarknodeRegistered(msg.sender, darknodeID, _minimumBond);
+ }
+
+ numDarknodesNextEpoch = numDarknodesNextEpoch.add(_darknodeIDs.length);
+ }
+
/// @notice Deregister a darknode. The darknode will not be deregistered
/// until the end of the epoch. After another epoch, the bond can be
/// refunded by calling the refund method.
@@ -291,6 +314,48 @@ contract DarknodeRegistryLogicV1 is
deregisterDarknode(_darknodeID);
}
+ /// @notice Deregister multiple darknodes. The darknodes will not be
+ /// deregistered until the end of the epoch. After another epoch, their
+ /// bonds can be refunded by calling the refund or refundMultiple methods.
+ /// If one deregistration fails, all deregistrations fail.
+ /// @param _darknodeIDs The darknode IDs that will be deregistered. The
+ /// caller of this method must be the owner of each darknode.
+ function deregisterMultiple(address[] calldata _darknodeIDs) external {
+ // Save variables in memory to prevent redundant reads from storage
+ DarknodeRegistryStore _store = store;
+ Epoch memory _currentEpoch = currentEpoch;
+ uint256 nextDeregisteredAt = _currentEpoch.blocktime.add(
+ minimumEpochInterval
+ );
+
+ for (uint256 i = 0; i < _darknodeIDs.length; i++) {
+ address darknodeID = _darknodeIDs[i];
+
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(darknodeID);
+ bool registered = isRegisteredInEpoch(
+ _store.darknodeRegisteredAt(darknodeID),
+ deregisteredAt,
+ _currentEpoch
+ );
+
+ require(
+ _isDeregisterable(registered, deregisteredAt),
+ "DarknodeRegistry: must be deregisterable"
+ );
+
+ require(
+ _store.darknodeOperator(darknodeID) == msg.sender,
+ "DarknodeRegistry: must be darknode owner"
+ );
+
+ _store.updateDarknodeDeregisteredAt(darknodeID, nextDeregisteredAt);
+
+ emit LogDarknodeDeregistered(msg.sender, darknodeID);
+ }
+
+ numDarknodesNextEpoch = numDarknodesNextEpoch.sub(_darknodeIDs.length);
+ }
+
/// @notice Progress the epoch if it is possible to do so. This captures
/// the current timestamp and current blockhash and overrides the current
/// epoch.
@@ -341,9 +406,6 @@ contract DarknodeRegistryLogicV1 is
slasher = nextSlasher;
emit LogSlasherUpdated(address(slasher), address(nextSlasher));
}
- if (address(darknodePayment) != address(0x0)) {
- darknodePayment.changeCycle();
- }
// Emit an event
emit LogNewEpoch(epochhash);
@@ -352,7 +414,7 @@ contract DarknodeRegistryLogicV1 is
/// @notice Allows the contract owner to initiate an ownership transfer of
/// the DarknodeRegistryStore.
/// @param _newOwner The address to transfer the ownership to.
- function transferStoreOwnership(DarknodeRegistryLogicV1 _newOwner)
+ function transferStoreOwnership(DarknodeRegistryLogicV2 _newOwner)
external
onlyOwner
{
@@ -375,26 +437,6 @@ contract DarknodeRegistryLogicV1 is
) = getDarknodeCountFromEpochs();
}
- /// @notice Allows the contract owner to update the address of the
- /// darknode payment contract.
- /// @param _darknodePayment The address of the Darknode Payment
- /// contract.
- function updateDarknodePayment(IDarknodePayment _darknodePayment)
- external
- onlyOwner
- {
- require(
- address(_darknodePayment) != address(0x0),
- "DarknodeRegistry: invalid Darknode Payment address"
- );
- IDarknodePayment previousDarknodePayment = darknodePayment;
- darknodePayment = _darknodePayment;
- emit LogDarknodePaymentUpdated(
- previousDarknodePayment,
- darknodePayment
- );
- }
-
/// @notice Allows the contract owner to update the minimum bond.
/// @param _nextMinimumBond The minimum bond amount that can be submitted by
/// a darknode.
@@ -427,10 +469,6 @@ contract DarknodeRegistryLogicV1 is
/// address.
/// @param _slasher The new slasher address.
function updateSlasher(IDarknodeSlasher _slasher) external onlyOwner {
- require(
- address(_slasher) != address(0),
- "DarknodeRegistry: invalid slasher address"
- );
nextSlasher = _slasher;
}
@@ -454,26 +492,19 @@ contract DarknodeRegistryLogicV1 is
uint256 totalBond = store.darknodeBond(_guilty);
uint256 penalty = totalBond.div(100).mul(_percentage);
uint256 challengerReward = penalty.div(2);
- uint256 darknodePaymentReward = penalty.sub(challengerReward);
+ uint256 slasherPortion = penalty.sub(challengerReward);
if (challengerReward > 0) {
// Slash the bond of the failed prover
store.updateDarknodeBond(_guilty, totalBond.sub(penalty));
- // Distribute the remaining bond into the darknode payment reward pool
+ // Forward the remaining amount to be handled by the slasher.
require(
- address(darknodePayment) != address(0x0),
- "DarknodeRegistry: invalid payment address"
- );
- require(
- ren.transfer(
- address(darknodePayment.store()),
- darknodePaymentReward
- ),
- "DarknodeRegistry: reward transfer failed"
+ ren.transfer(msg.sender, slasherPortion),
+ "DarknodeRegistry: reward transfer to slasher failed"
);
require(
ren.transfer(_challenger, challengerReward),
- "DarknodeRegistry: reward transfer failed"
+ "DarknodeRegistry: reward transfer to challenger failed"
);
}
@@ -486,13 +517,67 @@ contract DarknodeRegistryLogicV1 is
}
/// @notice Refund the bond of a deregistered darknode. This will make the
- /// darknode available for registration again. Anyone can call this function
- /// but the bond will always be refunded to the darknode operator.
+ /// darknode available for registration again.
///
/// @param _darknodeID The darknode ID that will be refunded.
- function refund(address _darknodeID) external onlyRefundable(_darknodeID) {
+ function refund(address _darknodeID)
+ external
+ onlyRefundable(_darknodeID)
+ onlyDarknodeOperator(_darknodeID)
+ {
+ // Remember the bond amount
+ uint256 amount = store.darknodeBond(_darknodeID);
+
+ // Erase the darknode from the registry
+ store.removeDarknode(_darknodeID);
+
+ // Refund the operator by transferring REN
+ require(
+ ren.transfer(msg.sender, amount),
+ "DarknodeRegistry: bond transfer failed"
+ );
+
+ // Emit an event.
+ emit LogDarknodeRefunded(msg.sender, _darknodeID, amount);
+ }
+
+ /// @notice A permissioned method for refunding a darknode without the usual
+ /// delay. The operator must provide a signature of the darknode ID and the
+ /// bond recipient, but the call must come from the contract's owner. The
+ /// main use case is for when an operator's keys have been compromised,
+ /// allowing for the bonds to be recovered by the operator through the
+ /// GatewayRegistry's governance. It is expected that this process would
+ /// happen towards the end of the darknode's deregistered period, so that
+ /// a malicious operator can't use this to quickly exit their stake after
+ /// attempting an attack on the network. It's also expected that the
+ /// operator will not re-register the same darknode again.
+ function recover(
+ address _darknodeID,
+ address _bondRecipient,
+ bytes calldata _signature
+ ) external onlyOwner {
+ require(
+ isRefundable(_darknodeID) || isDeregistered(_darknodeID),
+ "DarknodeRegistry: must be deregistered"
+ );
+
address darknodeOperator = store.darknodeOperator(_darknodeID);
+ require(
+ ECDSA.recover(
+ keccak256(
+ abi.encodePacked(
+ "\x19Ethereum Signed Message:\n64",
+ "DarknodeRegistry.recover",
+ _darknodeID,
+ _bondRecipient
+ )
+ ),
+ _signature
+ ) == darknodeOperator,
+ "DarknodeRegistry: invalid signature"
+ );
+
// Remember the bond amount
uint256 amount = store.darknodeBond(_darknodeID);
@@ -501,12 +586,73 @@ contract DarknodeRegistryLogicV1 is
// Refund the operator by transferring REN
require(
- ren.transfer(darknodeOperator, amount),
+ ren.transfer(_bondRecipient, amount),
"DarknodeRegistry: bond transfer failed"
);
// Emit an event.
emit LogDarknodeRefunded(darknodeOperator, _darknodeID, amount);
+ emit LogDarknodeRecovered(
+ darknodeOperator,
+ _darknodeID,
+ _bondRecipient,
+ msg.sender
+ );
+ }
+
+ /// @notice Refund the bonds of multiple deregistered darknodes. This will
+ /// make the darknodes available for registration again. If one refund fails,
+ /// all refunds fail.
+ /// @param _darknodeIDs The darknode IDs that will be refunded.
+ function refundMultiple(address[] calldata _darknodeIDs) external {
+ // Save variables in memory to prevent redundant reads from storage
+ DarknodeRegistryStore _store = store;
+ Epoch memory _currentEpoch = currentEpoch;
+ Epoch memory _previousEpoch = previousEpoch;
+ uint256 _deregistrationInterval = deregistrationInterval;
+
+ // The sum of bonds to refund
+ uint256 sum;
+
+ for (uint256 i = 0; i < _darknodeIDs.length; i++) {
+ address darknodeID = _darknodeIDs[i];
+
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(darknodeID);
+ bool deregistered = _isDeregistered(deregisteredAt, _currentEpoch);
+
+ require(
+ _isRefundable(
+ deregistered,
+ deregisteredAt,
+ _previousEpoch,
+ _deregistrationInterval
+ ),
+ "DarknodeRegistry: must be deregistered for at least one epoch"
+ );
+
+ require(
+ _store.darknodeOperator(darknodeID) == msg.sender,
+ "DarknodeRegistry: must be darknode owner"
+ );
+
+ // Remember the bond amount
+ uint256 amount = _store.darknodeBond(darknodeID);
+
+ // Erase the darknode from the registry
+ _store.removeDarknode(darknodeID);
+
+ // Emit an event
+ emit LogDarknodeRefunded(msg.sender, darknodeID, amount);
+
+ // Increment the sum of bonds to be transferred
+ sum = sum.add(amount);
+ }
+
+ // Transfer all bonds together
+ require(
+ ren.transfer(msg.sender, sum),
+ "DarknodeRegistry: bond transfers failed"
+ );
}
/// @notice Retrieves the address of the account that registered a darknode.
@@ -601,35 +747,46 @@ contract DarknodeRegistryLogicV1 is
/// @notice Returns if a darknode is in the deregistered state.
function isDeregistered(address _darknodeID) public view returns (bool) {
uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
- return deregisteredAt != 0 && deregisteredAt <= currentEpoch.blocktime;
+ return _isDeregistered(deregisteredAt, currentEpoch);
}
/// @notice Returns if a darknode can be deregistered. This is true if the
/// darknodes is in the registered state and has not attempted to
/// deregister yet.
function isDeregisterable(address _darknodeID) public view returns (bool) {
- uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
- // The Darknode is currently in the registered state and has not been
- // transitioned to the pending deregistration, or deregistered, state
- return isRegistered(_darknodeID) && deregisteredAt == 0;
+ DarknodeRegistryStore _store = store;
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(_darknodeID);
+ bool registered = isRegisteredInEpoch(
+ _store.darknodeRegisteredAt(_darknodeID),
+ deregisteredAt,
+ currentEpoch
+ );
+ return _isDeregisterable(registered, deregisteredAt);
}
/// @notice Returns if a darknode is in the refunded state. This is true
/// for darknodes that have never been registered, or darknodes that have
/// been deregistered and refunded.
function isRefunded(address _darknodeID) public view returns (bool) {
- uint256 registeredAt = store.darknodeRegisteredAt(_darknodeID);
- uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
- return registeredAt == 0 && deregisteredAt == 0;
+ DarknodeRegistryStore _store = store;
+ uint256 registeredAt = _store.darknodeRegisteredAt(_darknodeID);
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(_darknodeID);
+ return _isRefunded(registeredAt, deregisteredAt);
}
/// @notice Returns if a darknode is refundable. This is true for darknodes
/// that have been in the deregistered state for one full epoch.
function isRefundable(address _darknodeID) public view returns (bool) {
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ bool deregistered = _isDeregistered(deregisteredAt, currentEpoch);
+
return
- isDeregistered(_darknodeID) &&
- store.darknodeDeregisteredAt(_darknodeID) <=
- (previousEpoch.blocktime - deregistrationInterval);
+ _isRefundable(
+ deregistered,
+ deregisteredAt,
+ previousEpoch,
+ deregistrationInterval
+ );
}
/// @notice Returns the registration time of a given darknode.
@@ -652,7 +809,10 @@ contract DarknodeRegistryLogicV1 is
/// @notice Returns if a darknode is in the registered state.
function isRegistered(address _darknodeID) public view returns (bool) {
- return isRegisteredInEpoch(_darknodeID, currentEpoch);
+ DarknodeRegistryStore _store = store;
+ uint256 registeredAt = _store.darknodeRegisteredAt(_darknodeID);
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(_darknodeID);
+ return isRegisteredInEpoch(registeredAt, deregisteredAt, currentEpoch);
}
/// @notice Returns if a darknode was in the registered state last epoch.
@@ -661,28 +821,100 @@ contract DarknodeRegistryLogicV1 is
view
returns (bool)
{
- return isRegisteredInEpoch(_darknodeID, previousEpoch);
+ DarknodeRegistryStore _store = store;
+ uint256 registeredAt = _store.darknodeRegisteredAt(_darknodeID);
+ uint256 deregisteredAt = _store.darknodeDeregisteredAt(_darknodeID);
+ return isRegisteredInEpoch(registeredAt, deregisteredAt, previousEpoch);
+ }
+
+ function getOperatorDarknodes(address _operator)
+ public
+ view
+ returns (address[] memory)
+ {
+ address[] memory nodesPadded = new address[](numDarknodes);
+
+ address[] memory allNodes = getDarknodesFromEpochs(
+ address(0),
+ numDarknodes,
+ false
+ );
+
+ uint256 j = 0;
+ for (uint256 i = 0; i < allNodes.length; i++) {
+ if (store.darknodeOperator(allNodes[i]) == _operator) {
+ nodesPadded[j] = (allNodes[i]);
+ j++;
+ }
+ }
+
+ address[] memory nodes = new address[](j);
+ for (uint256 i = 0; i < j; i++) {
+ nodes[i] = nodesPadded[i];
+ }
+
+ return nodes;
}
/// @notice Returns if a darknode was in the registered state for a given
/// epoch.
- /// @param _darknodeID The ID of the darknode.
/// @param _epoch One of currentEpoch, previousEpoch.
- function isRegisteredInEpoch(address _darknodeID, Epoch memory _epoch)
- private
- view
- returns (bool)
- {
- uint256 registeredAt = store.darknodeRegisteredAt(_darknodeID);
- uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
- bool registered = registeredAt != 0 && registeredAt <= _epoch.blocktime;
- bool notDeregistered =
- deregisteredAt == 0 || deregisteredAt > _epoch.blocktime;
+ function isRegisteredInEpoch(
+ uint256 _registeredAt,
+ uint256 _deregisteredAt,
+ Epoch memory _epoch
+ ) private pure returns (bool) {
+ bool registered = _registeredAt != 0 &&
+ _registeredAt <= _epoch.blocktime;
+ bool notDeregistered = _deregisteredAt == 0 ||
+ _deregisteredAt > _epoch.blocktime;
// The Darknode has been registered and has not yet been deregistered,
// although it might be pending deregistration
return registered && notDeregistered;
}
+ /// Private function called by `isDeregistered`, `isRefundable`, and `refundMultiple`.
+ function _isDeregistered(
+ uint256 _deregisteredAt,
+ Epoch memory _currentEpoch
+ ) private pure returns (bool) {
+ return
+ _deregisteredAt != 0 && _deregisteredAt <= _currentEpoch.blocktime;
+ }
+
+ /// Private function called by `isDeregisterable` and `deregisterMultiple`.
+ function _isDeregisterable(bool _registered, uint256 _deregisteredAt)
+ private
+ pure
+ returns (bool)
+ {
+ // The Darknode is currently in the registered state and has not been
+ // transitioned to the pending deregistration, or deregistered, state
+ return _registered && _deregisteredAt == 0;
+ }
+
+ /// Private function called by `isRefunded` and `registerMultiple`.
+ function _isRefunded(uint256 registeredAt, uint256 deregisteredAt)
+ private
+ pure
+ returns (bool)
+ {
+ return registeredAt == 0 && deregisteredAt == 0;
+ }
+
+ /// Private function called by `isRefundable` and `refundMultiple`.
+ function _isRefundable(
+ bool _deregistered,
+ uint256 _deregisteredAt,
+ Epoch memory _previousEpoch,
+ uint256 _deregistrationInterval
+ ) private pure returns (bool) {
+ return
+ _deregistered &&
+ _deregisteredAt <=
+ (_previousEpoch.blocktime - _deregistrationInterval);
+ }
+
/// @notice Returns a list of darknodes registered for either the current
/// or the previous epoch. See `getDarknodes` for documentation on the
/// parameters `_start` and `_count`.
diff --git a/contracts/DarknodeRegistry/DarknodeRegistryV1.sol b/contracts/DarknodeRegistry/DarknodeRegistryV1.sol
new file mode 100644
index 00000000..c257ea67
--- /dev/null
+++ b/contracts/DarknodeRegistry/DarknodeRegistryV1.sol
@@ -0,0 +1,790 @@
+pragma solidity 0.5.17;
+
+import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
+import "@openzeppelin/upgrades/contracts/upgradeability/InitializableAdminUpgradeabilityProxy.sol";
+import "@openzeppelin/upgrades/contracts/Initializable.sol";
+
+import "../RenToken/RenToken.sol";
+import "./DarknodeRegistryStore.sol";
+import "../Governance/Claimable.sol";
+import "../libraries/CanReclaimTokens.sol";
+
+interface IDarknodePaymentStore {}
+
+interface IDarknodePayment {
+ function changeCycle() external returns (uint256);
+
+ function store() external view returns (IDarknodePaymentStore);
+}
+
+interface IDarknodeSlasher {}
+
+contract DarknodeRegistryStateV1 {
+ using SafeMath for uint256;
+
+ string public VERSION; // Passed in as a constructor parameter.
+
+ /// @notice Darknode pods are shuffled after a fixed number of blocks.
+ /// An Epoch stores an epoch hash used as an (insecure) RNG seed, and the
+ /// blocknumber which restricts when the next epoch can be called.
+ struct Epoch {
+ uint256 epochhash;
+ uint256 blocktime;
+ }
+
+ uint256 public numDarknodes;
+ uint256 public numDarknodesNextEpoch;
+ uint256 public numDarknodesPreviousEpoch;
+
+ /// Variables used to parameterize behavior.
+ uint256 public minimumBond;
+ uint256 public minimumPodSize;
+ uint256 public minimumEpochInterval;
+ uint256 public deregistrationInterval;
+
+ /// When one of the above variables is modified, it is only updated when the
+ /// next epoch is called. These variables store the values for the next
+ /// epoch.
+ uint256 public nextMinimumBond;
+ uint256 public nextMinimumPodSize;
+ uint256 public nextMinimumEpochInterval;
+
+ /// The current and previous epoch.
+ Epoch public currentEpoch;
+ Epoch public previousEpoch;
+
+ /// REN ERC20 contract used to transfer bonds.
+ RenToken public ren;
+
+ /// Darknode Registry Store is the storage contract for darknodes.
+ DarknodeRegistryStore public store;
+
+ /// The Darknode Payment contract for changing cycle.
+ IDarknodePayment public darknodePayment;
+
+ /// Darknode Slasher allows darknodes to vote on bond slashing.
+ IDarknodeSlasher public slasher;
+ IDarknodeSlasher public nextSlasher;
+}
+
+/// @notice DarknodeRegistry is responsible for the registration and
+/// deregistration of Darknodes.
+contract DarknodeRegistryLogicV1 is
+ Claimable,
+ CanReclaimTokens,
+ DarknodeRegistryStateV1
+{
+ /// @notice Emitted when a darknode is registered.
+ /// @param _darknodeOperator The owner of the darknode.
+ /// @param _darknodeID The ID of the darknode that was registered.
+ /// @param _bond The amount of REN that was transferred as bond.
+ event LogDarknodeRegistered(
+ address indexed _darknodeOperator,
+ address indexed _darknodeID,
+ uint256 _bond
+ );
+
+ /// @notice Emitted when a darknode is deregistered.
+ /// @param _darknodeOperator The owner of the darknode.
+ /// @param _darknodeID The ID of the darknode that was deregistered.
+ event LogDarknodeDeregistered(
+ address indexed _darknodeOperator,
+ address indexed _darknodeID
+ );
+
+ /// @notice Emitted when a refund has been made.
+ /// @param _darknodeOperator The owner of the darknode.
+ /// @param _amount The amount of REN that was refunded.
+ event LogDarknodeRefunded(
+ address indexed _darknodeOperator,
+ address indexed _darknodeID,
+ uint256 _amount
+ );
+
+ /// @notice Emitted when a darknode's bond is slashed.
+ /// @param _darknodeOperator The owner of the darknode.
+ /// @param _darknodeID The ID of the darknode that was slashed.
+ /// @param _challenger The address of the account that submitted the challenge.
+ /// @param _percentage The total percentage of bond slashed.
+ event LogDarknodeSlashed(
+ address indexed _darknodeOperator,
+ address indexed _darknodeID,
+ address indexed _challenger,
+ uint256 _percentage
+ );
+
+ /// @notice Emitted when a new epoch has begun.
+ event LogNewEpoch(uint256 indexed epochhash);
+
+ /// @notice Emitted when a constructor parameter has been updated.
+ event LogMinimumBondUpdated(
+ uint256 _previousMinimumBond,
+ uint256 _nextMinimumBond
+ );
+ event LogMinimumPodSizeUpdated(
+ uint256 _previousMinimumPodSize,
+ uint256 _nextMinimumPodSize
+ );
+ event LogMinimumEpochIntervalUpdated(
+ uint256 _previousMinimumEpochInterval,
+ uint256 _nextMinimumEpochInterval
+ );
+ event LogSlasherUpdated(
+ address indexed _previousSlasher,
+ address indexed _nextSlasher
+ );
+ event LogDarknodePaymentUpdated(
+ IDarknodePayment indexed _previousDarknodePayment,
+ IDarknodePayment indexed _nextDarknodePayment
+ );
+
+ /// @notice Restrict a function to the owner that registered the darknode.
+ modifier onlyDarknodeOperator(address _darknodeID) {
+ require(
+ store.darknodeOperator(_darknodeID) == msg.sender,
+ "DarknodeRegistry: must be darknode owner"
+ );
+ _;
+ }
+
+ /// @notice Restrict a function to unregistered darknodes.
+ modifier onlyRefunded(address _darknodeID) {
+ require(
+ isRefunded(_darknodeID),
+ "DarknodeRegistry: must be refunded or never registered"
+ );
+ _;
+ }
+
+ /// @notice Restrict a function to refundable darknodes.
+ modifier onlyRefundable(address _darknodeID) {
+ require(
+ isRefundable(_darknodeID),
+ "DarknodeRegistry: must be deregistered for at least one epoch"
+ );
+ _;
+ }
+
+ /// @notice Restrict a function to registered nodes without a pending
+ /// deregistration.
+ modifier onlyDeregisterable(address _darknodeID) {
+ require(
+ isDeregisterable(_darknodeID),
+ "DarknodeRegistry: must be deregisterable"
+ );
+ _;
+ }
+
+ /// @notice Restrict a function to the Slasher contract.
+ modifier onlySlasher() {
+ require(
+ address(slasher) == msg.sender,
+ "DarknodeRegistry: must be slasher"
+ );
+ _;
+ }
+
+ /// @notice Restrict a function to registered nodes without a pending
+ /// deregistration.
+ modifier onlyDarknode(address _darknodeID) {
+ require(
+ isRegistered(_darknodeID),
+ "DarknodeRegistry: invalid darknode"
+ );
+ _;
+ }
+
+ /// @notice The contract constructor.
+ ///
+ /// @param _VERSION A string defining the contract version.
+ /// @param _renAddress The address of the RenToken contract.
+ /// @param _storeAddress The address of the DarknodeRegistryStore contract.
+ /// @param _minimumBond The minimum bond amount that can be submitted by a
+ /// Darknode.
+ /// @param _minimumPodSize The minimum size of a Darknode pod.
+ /// @param _minimumEpochIntervalSeconds The minimum number of seconds between epochs.
+ function initialize(
+ string memory _VERSION,
+ RenToken _renAddress,
+ DarknodeRegistryStore _storeAddress,
+ uint256 _minimumBond,
+ uint256 _minimumPodSize,
+ uint256 _minimumEpochIntervalSeconds,
+ uint256 _deregistrationIntervalSeconds
+ ) public initializer {
+ Claimable.initialize(msg.sender);
+ CanReclaimTokens.initialize(msg.sender);
+ VERSION = _VERSION;
+
+ store = _storeAddress;
+ ren = _renAddress;
+
+ minimumBond = _minimumBond;
+ nextMinimumBond = minimumBond;
+
+ minimumPodSize = _minimumPodSize;
+ nextMinimumPodSize = minimumPodSize;
+
+ minimumEpochInterval = _minimumEpochIntervalSeconds;
+ nextMinimumEpochInterval = minimumEpochInterval;
+ deregistrationInterval = _deregistrationIntervalSeconds;
+
+ uint256 epochhash = uint256(blockhash(block.number - 1));
+ currentEpoch = Epoch({
+ epochhash: epochhash,
+ blocktime: block.timestamp
+ });
+ emit LogNewEpoch(epochhash);
+ }
+
+ /// @notice Register a darknode and transfer the bond to this contract.
+ /// Before registering, the bond transfer must be approved in the REN
+ /// contract. The caller must provide a public encryption key for the
+ /// darknode. The darknode will remain pending registration until the next
+ /// epoch. Only after this period can the darknode be deregistered. The
+ /// caller of this method will be stored as the owner of the darknode.
+ ///
+ /// @param _darknodeID The darknode ID that will be registered.
+ /// @param _publicKey The public key of the darknode. It is stored to allow
+ /// other darknodes and traders to encrypt messages to the trader.
+ function register(address _darknodeID, bytes calldata _publicKey)
+ external
+ onlyRefunded(_darknodeID)
+ {
+ require(
+ _darknodeID != address(0),
+ "DarknodeRegistry: darknode address cannot be zero"
+ );
+
+ // Use the current minimum bond as the darknode's bond and transfer bond to store
+ require(
+ ren.transferFrom(msg.sender, address(store), minimumBond),
+ "DarknodeRegistry: bond transfer failed"
+ );
+
+ // Flag this darknode for registration
+ store.appendDarknode(
+ _darknodeID,
+ msg.sender,
+ minimumBond,
+ _publicKey,
+ currentEpoch.blocktime.add(minimumEpochInterval),
+ 0
+ );
+
+ numDarknodesNextEpoch = numDarknodesNextEpoch.add(1);
+
+ // Emit an event.
+ emit LogDarknodeRegistered(msg.sender, _darknodeID, minimumBond);
+ }
+
+ /// @notice Deregister a darknode. The darknode will not be deregistered
+ /// until the end of the epoch. After another epoch, the bond can be
+ /// refunded by calling the refund method.
+ /// @param _darknodeID The darknode ID that will be deregistered. The caller
+ /// of this method must be the owner of this darknode.
+ function deregister(address _darknodeID)
+ external
+ onlyDeregisterable(_darknodeID)
+ onlyDarknodeOperator(_darknodeID)
+ {
+ deregisterDarknode(_darknodeID);
+ }
+
+ /// @notice Progress the epoch if it is possible to do so. This captures
+ /// the current timestamp and current blockhash and overrides the current
+ /// epoch.
+ function epoch() external {
+ if (previousEpoch.blocktime == 0) {
+ // The first epoch must be called by the owner of the contract
+ require(
+ msg.sender == owner(),
+ "DarknodeRegistry: not authorized to call first epoch"
+ );
+ }
+
+ // Require that the epoch interval has passed
+ require(
+ block.timestamp >= currentEpoch.blocktime.add(minimumEpochInterval),
+ "DarknodeRegistry: epoch interval has not passed"
+ );
+ uint256 epochhash = uint256(blockhash(block.number - 1));
+
+ // Update the epoch hash and timestamp
+ previousEpoch = currentEpoch;
+ currentEpoch = Epoch({
+ epochhash: epochhash,
+ blocktime: block.timestamp
+ });
+
+ // Update the registry information
+ numDarknodesPreviousEpoch = numDarknodes;
+ numDarknodes = numDarknodesNextEpoch;
+
+ // If any update functions have been called, update the values now
+ if (nextMinimumBond != minimumBond) {
+ minimumBond = nextMinimumBond;
+ emit LogMinimumBondUpdated(minimumBond, nextMinimumBond);
+ }
+ if (nextMinimumPodSize != minimumPodSize) {
+ minimumPodSize = nextMinimumPodSize;
+ emit LogMinimumPodSizeUpdated(minimumPodSize, nextMinimumPodSize);
+ }
+ if (nextMinimumEpochInterval != minimumEpochInterval) {
+ minimumEpochInterval = nextMinimumEpochInterval;
+ emit LogMinimumEpochIntervalUpdated(
+ minimumEpochInterval,
+ nextMinimumEpochInterval
+ );
+ }
+ if (nextSlasher != slasher) {
+ slasher = nextSlasher;
+ emit LogSlasherUpdated(address(slasher), address(nextSlasher));
+ }
+ if (address(darknodePayment) != address(0x0)) {
+ darknodePayment.changeCycle();
+ }
+
+ // Emit an event
+ emit LogNewEpoch(epochhash);
+ }
+
+ /// @notice Allows the contract owner to initiate an ownership transfer of
+ /// the DarknodeRegistryStore.
+ /// @param _newOwner The address to transfer the ownership to.
+ function transferStoreOwnership(DarknodeRegistryLogicV1 _newOwner)
+ external
+ onlyOwner
+ {
+ store.transferOwnership(address(_newOwner));
+ _newOwner.claimStoreOwnership();
+ }
+
+ /// @notice Claims ownership of the store passed in to the constructor.
+ /// `transferStoreOwnership` must have previously been called when
+ /// transferring from another Darknode Registry.
+ function claimStoreOwnership() external {
+ store.claimOwnership();
+
+ // Sync state with new store.
+ // Note: numDarknodesPreviousEpoch is set to 0 for a newly deployed DNR.
+ (
+ numDarknodesPreviousEpoch,
+ numDarknodes,
+ numDarknodesNextEpoch
+ ) = getDarknodeCountFromEpochs();
+ }
+
+ /// @notice Allows the contract owner to update the address of the
+ /// darknode payment contract.
+ /// @param _darknodePayment The address of the Darknode Payment
+ /// contract.
+ function updateDarknodePayment(IDarknodePayment _darknodePayment)
+ external
+ onlyOwner
+ {
+ require(
+ address(_darknodePayment) != address(0x0),
+ "DarknodeRegistry: invalid Darknode Payment address"
+ );
+ IDarknodePayment previousDarknodePayment = darknodePayment;
+ darknodePayment = _darknodePayment;
+ emit LogDarknodePaymentUpdated(
+ previousDarknodePayment,
+ darknodePayment
+ );
+ }
+
+ /// @notice Allows the contract owner to update the minimum bond.
+ /// @param _nextMinimumBond The minimum bond amount that can be submitted by
+ /// a darknode.
+ function updateMinimumBond(uint256 _nextMinimumBond) external onlyOwner {
+ // Will be updated next epoch
+ nextMinimumBond = _nextMinimumBond;
+ }
+
+ /// @notice Allows the contract owner to update the minimum pod size.
+ /// @param _nextMinimumPodSize The minimum size of a pod.
+ function updateMinimumPodSize(uint256 _nextMinimumPodSize)
+ external
+ onlyOwner
+ {
+ // Will be updated next epoch
+ nextMinimumPodSize = _nextMinimumPodSize;
+ }
+
+ /// @notice Allows the contract owner to update the minimum epoch interval.
+ /// @param _nextMinimumEpochInterval The minimum number of blocks between epochs.
+ function updateMinimumEpochInterval(uint256 _nextMinimumEpochInterval)
+ external
+ onlyOwner
+ {
+ // Will be updated next epoch
+ nextMinimumEpochInterval = _nextMinimumEpochInterval;
+ }
+
+ /// @notice Allow the contract owner to update the DarknodeSlasher contract
+ /// address.
+ /// @param _slasher The new slasher address.
+ function updateSlasher(IDarknodeSlasher _slasher) external onlyOwner {
+ require(
+ address(_slasher) != address(0),
+ "DarknodeRegistry: invalid slasher address"
+ );
+ nextSlasher = _slasher;
+ }
+
+ /// @notice Allow the DarknodeSlasher contract to slash a portion of darknode's
+ /// bond and deregister it.
+ /// @param _guilty The guilty prover whose bond is being slashed.
+ /// @param _challenger The challenger who should receive a portion of the bond as reward.
+ /// @param _percentage The total percentage of bond to be slashed.
+ function slash(
+ address _guilty,
+ address _challenger,
+ uint256 _percentage
+ ) external onlySlasher onlyDarknode(_guilty) {
+ require(_percentage <= 100, "DarknodeRegistry: invalid percent");
+
+ // If the darknode has not been deregistered then deregister it
+ if (isDeregisterable(_guilty)) {
+ deregisterDarknode(_guilty);
+ }
+
+ uint256 totalBond = store.darknodeBond(_guilty);
+ uint256 penalty = totalBond.div(100).mul(_percentage);
+ uint256 challengerReward = penalty.div(2);
+ uint256 darknodePaymentReward = penalty.sub(challengerReward);
+ if (challengerReward > 0) {
+ // Slash the bond of the failed prover
+ store.updateDarknodeBond(_guilty, totalBond.sub(penalty));
+
+ // Distribute the remaining bond into the darknode payment reward pool
+ require(
+ address(darknodePayment) != address(0x0),
+ "DarknodeRegistry: invalid payment address"
+ );
+ require(
+ ren.transfer(
+ address(darknodePayment.store()),
+ darknodePaymentReward
+ ),
+ "DarknodeRegistry: reward transfer failed"
+ );
+ require(
+ ren.transfer(_challenger, challengerReward),
+ "DarknodeRegistry: reward transfer failed"
+ );
+ }
+
+ emit LogDarknodeSlashed(
+ store.darknodeOperator(_guilty),
+ _guilty,
+ _challenger,
+ _percentage
+ );
+ }
+
+ /// @notice Refund the bond of a deregistered darknode. This will make the
+ /// darknode available for registration again. Anyone can call this function
+ /// but the bond will always be refunded to the darknode operator.
+ ///
+ /// @param _darknodeID The darknode ID that will be refunded.
+ function refund(address _darknodeID) external onlyRefundable(_darknodeID) {
+ address darknodeOperator = store.darknodeOperator(_darknodeID);
+
+ // Remember the bond amount
+ uint256 amount = store.darknodeBond(_darknodeID);
+
+ // Erase the darknode from the registry
+ store.removeDarknode(_darknodeID);
+
+ // Refund the operator by transferring REN
+ require(
+ ren.transfer(darknodeOperator, amount),
+ "DarknodeRegistry: bond transfer failed"
+ );
+
+ // Emit an event.
+ emit LogDarknodeRefunded(darknodeOperator, _darknodeID, amount);
+ }
+
+ /// @notice Retrieves the address of the account that registered a darknode.
+ /// @param _darknodeID The ID of the darknode to retrieve the owner for.
+ function getDarknodeOperator(address _darknodeID)
+ external
+ view
+ returns (address payable)
+ {
+ return store.darknodeOperator(_darknodeID);
+ }
+
+ /// @notice Retrieves the bond amount of a darknode in 10^-18 REN.
+ /// @param _darknodeID The ID of the darknode to retrieve the bond for.
+ function getDarknodeBond(address _darknodeID)
+ external
+ view
+ returns (uint256)
+ {
+ return store.darknodeBond(_darknodeID);
+ }
+
+ /// @notice Retrieves the encryption public key of the darknode.
+ /// @param _darknodeID The ID of the darknode to retrieve the public key for.
+ function getDarknodePublicKey(address _darknodeID)
+ external
+ view
+ returns (bytes memory)
+ {
+ return store.darknodePublicKey(_darknodeID);
+ }
+
+ /// @notice Retrieves a list of darknodes which are registered for the
+ /// current epoch.
+ /// @param _start A darknode ID used as an offset for the list. If _start is
+ /// 0x0, the first dark node will be used. _start won't be
+ /// included it is not registered for the epoch.
+ /// @param _count The number of darknodes to retrieve starting from _start.
+ /// If _count is 0, all of the darknodes from _start are
+ /// retrieved. If _count is more than the remaining number of
+ /// registered darknodes, the rest of the list will contain
+ /// 0x0s.
+ function getDarknodes(address _start, uint256 _count)
+ external
+ view
+ returns (address[] memory)
+ {
+ uint256 count = _count;
+ if (count == 0) {
+ count = numDarknodes;
+ }
+ return getDarknodesFromEpochs(_start, count, false);
+ }
+
+ /// @notice Retrieves a list of darknodes which were registered for the
+ /// previous epoch. See `getDarknodes` for the parameter documentation.
+ function getPreviousDarknodes(address _start, uint256 _count)
+ external
+ view
+ returns (address[] memory)
+ {
+ uint256 count = _count;
+ if (count == 0) {
+ count = numDarknodesPreviousEpoch;
+ }
+ return getDarknodesFromEpochs(_start, count, true);
+ }
+
+ /// @notice Returns whether a darknode is scheduled to become registered
+ /// at next epoch.
+ /// @param _darknodeID The ID of the darknode to return.
+ function isPendingRegistration(address _darknodeID)
+ public
+ view
+ returns (bool)
+ {
+ uint256 registeredAt = store.darknodeRegisteredAt(_darknodeID);
+ return registeredAt != 0 && registeredAt > currentEpoch.blocktime;
+ }
+
+ /// @notice Returns if a darknode is in the pending deregistered state. In
+ /// this state a darknode is still considered registered.
+ function isPendingDeregistration(address _darknodeID)
+ public
+ view
+ returns (bool)
+ {
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ return deregisteredAt != 0 && deregisteredAt > currentEpoch.blocktime;
+ }
+
+ /// @notice Returns if a darknode is in the deregistered state.
+ function isDeregistered(address _darknodeID) public view returns (bool) {
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ return deregisteredAt != 0 && deregisteredAt <= currentEpoch.blocktime;
+ }
+
+ /// @notice Returns if a darknode can be deregistered. This is true if the
+ /// darknodes is in the registered state and has not attempted to
+ /// deregister yet.
+ function isDeregisterable(address _darknodeID) public view returns (bool) {
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ // The Darknode is currently in the registered state and has not been
+ // transitioned to the pending deregistration, or deregistered, state
+ return isRegistered(_darknodeID) && deregisteredAt == 0;
+ }
+
+ /// @notice Returns if a darknode is in the refunded state. This is true
+ /// for darknodes that have never been registered, or darknodes that have
+ /// been deregistered and refunded.
+ function isRefunded(address _darknodeID) public view returns (bool) {
+ uint256 registeredAt = store.darknodeRegisteredAt(_darknodeID);
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ return registeredAt == 0 && deregisteredAt == 0;
+ }
+
+ /// @notice Returns if a darknode is refundable. This is true for darknodes
+ /// that have been in the deregistered state for one full epoch.
+ function isRefundable(address _darknodeID) public view returns (bool) {
+ return
+ isDeregistered(_darknodeID) &&
+ store.darknodeDeregisteredAt(_darknodeID) <=
+ (previousEpoch.blocktime - deregistrationInterval);
+ }
+
+ /// @notice Returns the registration time of a given darknode.
+ function darknodeRegisteredAt(address darknodeID)
+ external
+ view
+ returns (uint256)
+ {
+ return store.darknodeRegisteredAt(darknodeID);
+ }
+
+ /// @notice Returns the deregistration time of a given darknode.
+ function darknodeDeregisteredAt(address darknodeID)
+ external
+ view
+ returns (uint256)
+ {
+ return store.darknodeDeregisteredAt(darknodeID);
+ }
+
+ /// @notice Returns if a darknode is in the registered state.
+ function isRegistered(address _darknodeID) public view returns (bool) {
+ return isRegisteredInEpoch(_darknodeID, currentEpoch);
+ }
+
+ /// @notice Returns if a darknode was in the registered state last epoch.
+ function isRegisteredInPreviousEpoch(address _darknodeID)
+ public
+ view
+ returns (bool)
+ {
+ return isRegisteredInEpoch(_darknodeID, previousEpoch);
+ }
+
+ /// @notice Returns if a darknode was in the registered state for a given
+ /// epoch.
+ /// @param _darknodeID The ID of the darknode.
+ /// @param _epoch One of currentEpoch, previousEpoch.
+ function isRegisteredInEpoch(address _darknodeID, Epoch memory _epoch)
+ private
+ view
+ returns (bool)
+ {
+ uint256 registeredAt = store.darknodeRegisteredAt(_darknodeID);
+ uint256 deregisteredAt = store.darknodeDeregisteredAt(_darknodeID);
+ bool registered = registeredAt != 0 && registeredAt <= _epoch.blocktime;
+ bool notDeregistered = deregisteredAt == 0 ||
+ deregisteredAt > _epoch.blocktime;
+ // The Darknode has been registered and has not yet been deregistered,
+ // although it might be pending deregistration
+ return registered && notDeregistered;
+ }
+
+ /// @notice Returns a list of darknodes registered for either the current
+ /// or the previous epoch. See `getDarknodes` for documentation on the
+ /// parameters `_start` and `_count`.
+ /// @param _usePreviousEpoch If true, use the previous epoch, otherwise use
+ /// the current epoch.
+ function getDarknodesFromEpochs(
+ address _start,
+ uint256 _count,
+ bool _usePreviousEpoch
+ ) private view returns (address[] memory) {
+ uint256 count = _count;
+ if (count == 0) {
+ count = numDarknodes;
+ }
+
+ address[] memory nodes = new address[](count);
+
+ // Begin with the first node in the list
+ uint256 n = 0;
+ address next = _start;
+ if (next == address(0)) {
+ next = store.begin();
+ }
+
+ // Iterate until all registered Darknodes have been collected
+ while (n < count) {
+ if (next == address(0)) {
+ break;
+ }
+ // Only include Darknodes that are currently registered
+ bool includeNext;
+ if (_usePreviousEpoch) {
+ includeNext = isRegisteredInPreviousEpoch(next);
+ } else {
+ includeNext = isRegistered(next);
+ }
+ if (!includeNext) {
+ next = store.next(next);
+ continue;
+ }
+ nodes[n] = next;
+ next = store.next(next);
+ n += 1;
+ }
+ return nodes;
+ }
+
+ /// Private function called by `deregister` and `slash`
+ function deregisterDarknode(address _darknodeID) private {
+ address darknodeOperator = store.darknodeOperator(_darknodeID);
+
+ // Flag the darknode for deregistration
+ store.updateDarknodeDeregisteredAt(
+ _darknodeID,
+ currentEpoch.blocktime.add(minimumEpochInterval)
+ );
+ numDarknodesNextEpoch = numDarknodesNextEpoch.sub(1);
+
+ // Emit an event
+ emit LogDarknodeDeregistered(darknodeOperator, _darknodeID);
+ }
+
+ function getDarknodeCountFromEpochs()
+ private
+ view
+ returns (
+ uint256,
+ uint256,
+ uint256
+ )
+ {
+ // Begin with the first node in the list
+ uint256 nPreviousEpoch = 0;
+ uint256 nCurrentEpoch = 0;
+ uint256 nNextEpoch = 0;
+ address next = store.begin();
+
+ // Iterate until all registered Darknodes have been collected
+ while (true) {
+ // End of darknode list.
+ if (next == address(0)) {
+ break;
+ }
+
+ if (isRegisteredInPreviousEpoch(next)) {
+ nPreviousEpoch += 1;
+ }
+
+ if (isRegistered(next)) {
+ nCurrentEpoch += 1;
+ }
+
+ // Darknode is registered and has not deregistered, or is pending
+ // becoming registered.
+ if (
+ ((isRegistered(next) && !isPendingDeregistration(next)) ||
+ isPendingRegistration(next))
+ ) {
+ nNextEpoch += 1;
+ }
+ next = store.next(next);
+ }
+ return (nPreviousEpoch, nCurrentEpoch, nNextEpoch);
+ }
+}
diff --git a/contracts/DarknodeRegistry/DarknodeRegistryV1ToV2Upgrader.sol b/contracts/DarknodeRegistry/DarknodeRegistryV1ToV2Upgrader.sol
new file mode 100644
index 00000000..72099267
--- /dev/null
+++ b/contracts/DarknodeRegistry/DarknodeRegistryV1ToV2Upgrader.sol
@@ -0,0 +1,128 @@
+pragma solidity ^0.5.17;
+
+import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";
+
+import "./DarknodeRegistry.sol";
+
+import "../Governance/RenProxyAdmin.sol";
+
+contract DarknodeRegistryV1ToV2Upgrader is Ownable {
+ RenProxyAdmin public renProxyAdmin;
+ DarknodeRegistryLogicV1 public darknodeRegistryProxy;
+ DarknodeRegistryLogicV2 public darknodeRegistryLogicV2;
+ address public previousAdminOwner;
+ address public previousDarknodeRegistryOwner;
+
+ constructor(
+ RenProxyAdmin _renProxyAdmin,
+ DarknodeRegistryLogicV1 _darknodeRegistryProxy,
+ DarknodeRegistryLogicV2 _darknodeRegistryLogicV2
+ ) public {
+ Ownable.initialize(msg.sender);
+ renProxyAdmin = _renProxyAdmin;
+ darknodeRegistryProxy = _darknodeRegistryProxy;
+ darknodeRegistryLogicV2 = _darknodeRegistryLogicV2;
+ previousAdminOwner = renProxyAdmin.owner();
+ previousDarknodeRegistryOwner = darknodeRegistryProxy.owner();
+ }
+
+ function upgrade() public onlyOwner {
+ // Pre-checks
+ uint256 numDarknodes = darknodeRegistryProxy.numDarknodes();
+ uint256 numDarknodesNextEpoch = darknodeRegistryProxy
+ .numDarknodesNextEpoch();
+ uint256 numDarknodesPreviousEpoch = darknodeRegistryProxy
+ .numDarknodesPreviousEpoch();
+ uint256 minimumBond = darknodeRegistryProxy.minimumBond();
+ uint256 minimumPodSize = darknodeRegistryProxy.minimumPodSize();
+ uint256 minimumEpochInterval = darknodeRegistryProxy
+ .minimumEpochInterval();
+ uint256 deregistrationInterval = darknodeRegistryProxy
+ .deregistrationInterval();
+ RenToken ren = darknodeRegistryProxy.ren();
+ DarknodeRegistryStore store = darknodeRegistryProxy.store();
+ IDarknodePayment darknodePayment = darknodeRegistryProxy
+ .darknodePayment();
+
+ // Claim and update.
+ darknodeRegistryProxy.claimOwnership();
+ renProxyAdmin.upgrade(
+ AdminUpgradeabilityProxy(
+ // Cast gateway instance to payable address
+ address(uint160(address(darknodeRegistryProxy)))
+ ),
+ address(darknodeRegistryLogicV2)
+ );
+
+ // Post-checks
+ require(
+ numDarknodes == darknodeRegistryProxy.numDarknodes(),
+ "Migrator: expected 'numDarknodes' not to change"
+ );
+ require(
+ numDarknodesNextEpoch ==
+ darknodeRegistryProxy.numDarknodesNextEpoch(),
+ "Migrator: expected 'numDarknodesNextEpoch' not to change"
+ );
+ require(
+ numDarknodesPreviousEpoch ==
+ darknodeRegistryProxy.numDarknodesPreviousEpoch(),
+ "Migrator: expected 'numDarknodesPreviousEpoch' not to change"
+ );
+ require(
+ minimumBond == darknodeRegistryProxy.minimumBond(),
+ "Migrator: expected 'minimumBond' not to change"
+ );
+ require(
+ minimumPodSize == darknodeRegistryProxy.minimumPodSize(),
+ "Migrator: expected 'minimumPodSize' not to change"
+ );
+ require(
+ minimumEpochInterval ==
+ darknodeRegistryProxy.minimumEpochInterval(),
+ "Migrator: expected 'minimumEpochInterval' not to change"
+ );
+ require(
+ deregistrationInterval ==
+ darknodeRegistryProxy.deregistrationInterval(),
+ "Migrator: expected 'deregistrationInterval' not to change"
+ );
+ require(
+ ren == darknodeRegistryProxy.ren(),
+ "Migrator: expected 'ren' not to change"
+ );
+ require(
+ store == darknodeRegistryProxy.store(),
+ "Migrator: expected 'store' not to change"
+ );
+ require(
+ darknodePayment == darknodeRegistryProxy.darknodePayment(),
+ "Migrator: expected 'darknodePayment' not to change"
+ );
+
+ darknodeRegistryProxy.updateSlasher(IDarknodeSlasher(0x0));
+ }
+
+ function recover(
+ address _darknodeID,
+ address _bondRecipient,
+ bytes calldata _signature
+ ) external onlyOwner {
+ return
+ DarknodeRegistryLogicV2(address(darknodeRegistryProxy)).recover(
+ _darknodeID,
+ _bondRecipient,
+ _signature
+ );
+ }
+
+ function returnDNR() public onlyOwner {
+ darknodeRegistryProxy._directTransferOwnership(
+ previousDarknodeRegistryOwner
+ );
+ }
+
+ function returnProxyAdmin() public onlyOwner {
+ renProxyAdmin.transferOwnership(previousAdminOwner);
+ }
+}
diff --git a/contracts/DarknodeRegistry/GetOperatorDarknodes.sol b/contracts/DarknodeRegistry/GetOperatorDarknodes.sol
deleted file mode 100644
index c26e692a..00000000
--- a/contracts/DarknodeRegistry/GetOperatorDarknodes.sol
+++ /dev/null
@@ -1,42 +0,0 @@
-pragma solidity 0.5.17;
-
-import "./DarknodeRegistry.sol";
-
-contract GetOperatorDarknodes {
- DarknodeRegistryLogicV1 public darknodeRegistry;
-
- constructor(DarknodeRegistryLogicV1 _darknodeRegistry) public {
- darknodeRegistry = _darknodeRegistry;
- }
-
- function getOperatorDarknodes(address _operator)
- public
- view
- returns (address[] memory)
- {
- uint256 numDarknodes = darknodeRegistry.numDarknodes();
- address[] memory nodesPadded = new address[](numDarknodes);
-
- address[] memory allNodes = darknodeRegistry.getDarknodes(
- address(0),
- 0
- );
-
- uint256 j = 0;
- for (uint256 i = 0; i < allNodes.length; i++) {
- if (
- darknodeRegistry.getDarknodeOperator(allNodes[i]) == _operator
- ) {
- nodesPadded[j] = (allNodes[i]);
- j++;
- }
- }
-
- address[] memory nodes = new address[](j);
- for (uint256 i = 0; i < j; i++) {
- nodes[i] = nodesPadded[i];
- }
-
- return nodes;
- }
-}
diff --git a/contracts/DarknodeSlasher/DarknodeSlasher.sol b/contracts/DarknodeSlasher/DarknodeSlasher.sol
deleted file mode 100644
index 1ffc0280..00000000
--- a/contracts/DarknodeSlasher/DarknodeSlasher.sol
+++ /dev/null
@@ -1,199 +0,0 @@
-pragma solidity 0.5.17;
-
-import "../Governance/Claimable.sol";
-import "../libraries/Validate.sol";
-import "../DarknodeRegistry/DarknodeRegistry.sol";
-
-/// @notice DarknodeSlasher will become a voting system for darknodes to
-/// deregister other misbehaving darknodes.
-/// Right now, it is a placeholder.
-contract DarknodeSlasher is Claimable {
- DarknodeRegistryLogicV1 public darknodeRegistry;
-
- uint256 public blacklistSlashPercent;
- uint256 public maliciousSlashPercent;
- uint256 public secretRevealSlashPercent;
-
- // Malicious Darknodes can be slashed for each height and round
- // mapping of height -> round -> guilty address -> slashed
- mapping(uint256 => mapping(uint256 => mapping(address => bool)))
- public slashed;
-
- // mapping of darknodes which have revealed their secret
- mapping(address => bool) public secretRevealed;
-
- // mapping of address to whether the darknode has been blacklisted
- mapping(address => bool) public blacklisted;
-
- /// @notice Emitted when the DarknodeRegistry is updated.
- /// @param _previousDarknodeRegistry The address of the old registry.
- /// @param _nextDarknodeRegistry The address of the new registry.
- event LogDarknodeRegistryUpdated(
- DarknodeRegistryLogicV1 indexed _previousDarknodeRegistry,
- DarknodeRegistryLogicV1 indexed _nextDarknodeRegistry
- );
-
- /// @notice Restrict a function to have a valid percentage.
- modifier validPercent(uint256 _percent) {
- require(_percent <= 100, "DarknodeSlasher: invalid percentage");
- _;
- }
-
- constructor(DarknodeRegistryLogicV1 _darknodeRegistry) public {
- Claimable.initialize(msg.sender);
- darknodeRegistry = _darknodeRegistry;
- }
-
- /// @notice Allows the contract owner to update the address of the
- /// darknode registry contract.
- /// @param _darknodeRegistry The address of the Darknode Registry
- /// contract.
- function updateDarknodeRegistry(DarknodeRegistryLogicV1 _darknodeRegistry)
- external
- onlyOwner
- {
- require(
- address(_darknodeRegistry) != address(0x0),
- "DarknodeSlasher: invalid Darknode Registry address"
- );
- DarknodeRegistryLogicV1 previousDarknodeRegistry = darknodeRegistry;
- darknodeRegistry = _darknodeRegistry;
- emit LogDarknodeRegistryUpdated(
- previousDarknodeRegistry,
- darknodeRegistry
- );
- }
-
- function setBlacklistSlashPercent(uint256 _percentage)
- public
- validPercent(_percentage)
- onlyOwner
- {
- blacklistSlashPercent = _percentage;
- }
-
- function setMaliciousSlashPercent(uint256 _percentage)
- public
- validPercent(_percentage)
- onlyOwner
- {
- maliciousSlashPercent = _percentage;
- }
-
- function setSecretRevealSlashPercent(uint256 _percentage)
- public
- validPercent(_percentage)
- onlyOwner
- {
- secretRevealSlashPercent = _percentage;
- }
-
- function slash(
- address _guilty,
- address _challenger,
- uint256 _percentage
- ) external onlyOwner {
- darknodeRegistry.slash(_guilty, _challenger, _percentage);
- }
-
- function blacklist(address _guilty) external onlyOwner {
- require(!blacklisted[_guilty], "DarknodeSlasher: already blacklisted");
- blacklisted[_guilty] = true;
- darknodeRegistry.slash(_guilty, owner(), blacklistSlashPercent);
- }
-
- function slashDuplicatePropose(
- uint256 _height,
- uint256 _round,
- bytes calldata _blockhash1,
- uint256 _validRound1,
- bytes calldata _signature1,
- bytes calldata _blockhash2,
- uint256 _validRound2,
- bytes calldata _signature2
- ) external {
- address signer =
- Validate.duplicatePropose(
- _height,
- _round,
- _blockhash1,
- _validRound1,
- _signature1,
- _blockhash2,
- _validRound2,
- _signature2
- );
- require(
- !slashed[_height][_round][signer],
- "DarknodeSlasher: already slashed"
- );
- slashed[_height][_round][signer] = true;
- darknodeRegistry.slash(signer, msg.sender, maliciousSlashPercent);
- }
-
- function slashDuplicatePrevote(
- uint256 _height,
- uint256 _round,
- bytes calldata _blockhash1,
- bytes calldata _signature1,
- bytes calldata _blockhash2,
- bytes calldata _signature2
- ) external {
- address signer =
- Validate.duplicatePrevote(
- _height,
- _round,
- _blockhash1,
- _signature1,
- _blockhash2,
- _signature2
- );
- require(
- !slashed[_height][_round][signer],
- "DarknodeSlasher: already slashed"
- );
- slashed[_height][_round][signer] = true;
- darknodeRegistry.slash(signer, msg.sender, maliciousSlashPercent);
- }
-
- function slashDuplicatePrecommit(
- uint256 _height,
- uint256 _round,
- bytes calldata _blockhash1,
- bytes calldata _signature1,
- bytes calldata _blockhash2,
- bytes calldata _signature2
- ) external {
- address signer =
- Validate.duplicatePrecommit(
- _height,
- _round,
- _blockhash1,
- _signature1,
- _blockhash2,
- _signature2
- );
- require(
- !slashed[_height][_round][signer],
- "DarknodeSlasher: already slashed"
- );
- slashed[_height][_round][signer] = true;
- darknodeRegistry.slash(signer, msg.sender, maliciousSlashPercent);
- }
-
- function slashSecretReveal(
- uint256 _a,
- uint256 _b,
- uint256 _c,
- uint256 _d,
- uint256 _e,
- uint256 _f,
- bytes calldata _signature
- ) external {
- address signer =
- Validate.recoverSecret(_a, _b, _c, _d, _e, _f, _signature);
- require(!secretRevealed[signer], "DarknodeSlasher: already slashed");
- secretRevealed[signer] = true;
- darknodeRegistry.slash(signer, msg.sender, secretRevealSlashPercent);
- }
-}
diff --git a/contracts/Governance/Claimable.sol b/contracts/Governance/Claimable.sol
index 7b1f73e5..509ae7bb 100644
--- a/contracts/Governance/Claimable.sol
+++ b/contracts/Governance/Claimable.sol
@@ -31,6 +31,12 @@ contract Claimable is Initializable, Ownable {
pendingOwner = newOwner;
}
+ // Allow skipping two-step transfer if the recipient is known to be a valid
+ // owner, for use in smart-contracts only.
+ function _directTransferOwnership(address newOwner) public onlyOwner {
+ _transferOwnership(newOwner);
+ }
+
function claimOwnership() public onlyPendingOwner {
_transferOwnership(pendingOwner);
delete pendingOwner;
diff --git a/contracts/Protocol/Protocol.sol b/contracts/Protocol/Protocol.sol
deleted file mode 100644
index a5ddb643..00000000
--- a/contracts/Protocol/Protocol.sol
+++ /dev/null
@@ -1,49 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/upgrades/contracts/Initializable.sol";
-import "../Governance/Claimable.sol";
-
-/** The Protocol contract is used to look-up other Ren contracts. */
-contract Protocol is Initializable, Claimable {
- event LogContractUpdated(
- string contractName,
- address indexed contractAddress,
- string indexed contractNameIndexed
- );
-
- mapping(string => address) internal contractMap;
-
- function __Protocol_init(address adminAddress_) public initializer {
- Claimable.initialize(adminAddress_);
- }
-
- function addContract(string memory contractName, address contractAddress)
- public
- onlyOwner
- {
- require(
- contractMap[contractName] == address(0x0),
- "Protocol: contract entry already exists"
- );
- contractMap[contractName] = contractAddress;
-
- emit LogContractUpdated(contractName, contractAddress, contractName);
- }
-
- function updateContract(string memory contractName, address contractAddress)
- public
- onlyOwner
- {
- contractMap[contractName] = contractAddress;
-
- emit LogContractUpdated(contractName, contractAddress, contractName);
- }
-
- function getContract(string memory contractName)
- public
- view
- returns (address)
- {
- return contractMap[contractName];
- }
-}
diff --git a/contracts/libraries/Compare.sol b/contracts/libraries/Compare.sol
deleted file mode 100644
index 5593430e..00000000
--- a/contracts/libraries/Compare.sol
+++ /dev/null
@@ -1,19 +0,0 @@
-pragma solidity 0.5.17;
-
-library Compare {
- function bytesEqual(bytes memory a, bytes memory b)
- internal
- pure
- returns (bool)
- {
- if (a.length != b.length) {
- return false;
- }
- for (uint256 i = 0; i < a.length; i++) {
- if (a[i] != b[i]) {
- return false;
- }
- }
- return true;
- }
-}
diff --git a/contracts/libraries/ERC20WithFees.sol b/contracts/libraries/ERC20WithFees.sol
deleted file mode 100644
index d3c791cd..00000000
--- a/contracts/libraries/ERC20WithFees.sol
+++ /dev/null
@@ -1,25 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/math/Math.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-
-library ERC20WithFees {
- using SafeMath for uint256;
- using SafeERC20 for IERC20;
-
- /// @notice Calls transferFrom on the token, returning the value transferred
- /// after fees.
- function safeTransferFromWithFees(
- IERC20 token,
- address from,
- address to,
- uint256 value
- ) internal returns (uint256) {
- uint256 balancesBefore = token.balanceOf(to);
- token.safeTransferFrom(from, to, value);
- uint256 balancesAfter = token.balanceOf(to);
- return Math.min(value, balancesAfter.sub(balancesBefore));
- }
-}
diff --git a/contracts/libraries/String.sol b/contracts/libraries/String.sol
deleted file mode 100644
index d4798104..00000000
--- a/contracts/libraries/String.sol
+++ /dev/null
@@ -1,67 +0,0 @@
-pragma solidity 0.5.17;
-
-library String {
- /// @notice Convert a uint value to its decimal string representation
- // solium-disable-next-line security/no-assign-params
- function fromUint(uint256 _i) internal pure returns (string memory) {
- if (_i == 0) {
- return "0";
- }
- uint256 j = _i;
- uint256 len;
- while (j != 0) {
- len++;
- j /= 10;
- }
- bytes memory bstr = new bytes(len);
- uint256 k = len - 1;
- while (_i != 0) {
- bstr[k--] = bytes1(uint8(48 + (_i % 10)));
- _i /= 10;
- }
- return string(bstr);
- }
-
- /// @notice Convert a bytes32 value to its hex string representation.
- function fromBytes32(bytes32 _value) internal pure returns (string memory) {
- bytes memory alphabet = "0123456789abcdef";
-
- bytes memory str = new bytes(32 * 2 + 2);
- str[0] = "0";
- str[1] = "x";
- for (uint256 i = 0; i < 32; i++) {
- str[2 + i * 2] = alphabet[uint256(uint8(_value[i] >> 4))];
- str[3 + i * 2] = alphabet[uint256(uint8(_value[i] & 0x0f))];
- }
- return string(str);
- }
-
- /// @notice Convert an address to its hex string representation.
- function fromAddress(address _addr) internal pure returns (string memory) {
- bytes32 value = bytes32(uint256(_addr));
- bytes memory alphabet = "0123456789abcdef";
-
- bytes memory str = new bytes(20 * 2 + 2);
- str[0] = "0";
- str[1] = "x";
- for (uint256 i = 0; i < 20; i++) {
- str[2 + i * 2] = alphabet[uint256(uint8(value[i + 12] >> 4))];
- str[3 + i * 2] = alphabet[uint256(uint8(value[i + 12] & 0x0f))];
- }
- return string(str);
- }
-
- /// @notice Append eight strings.
- function add8(
- string memory a,
- string memory b,
- string memory c,
- string memory d,
- string memory e,
- string memory f,
- string memory g,
- string memory h
- ) internal pure returns (string memory) {
- return string(abi.encodePacked(a, b, c, d, e, f, g, h));
- }
-}
diff --git a/contracts/libraries/Validate.sol b/contracts/libraries/Validate.sol
deleted file mode 100644
index b520a7bd..00000000
--- a/contracts/libraries/Validate.sol
+++ /dev/null
@@ -1,244 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/cryptography/ECDSA.sol";
-
-import "../libraries/String.sol";
-import "../libraries/Compare.sol";
-
-/// @notice Validate is a library for validating malicious darknode behaviour.
-library Validate {
- /// @notice Recovers two propose messages and checks if they were signed by
- /// the same darknode. If they were different but the height and
- /// round were the same, then the darknode was behaving maliciously.
- /// @return The address of the signer if and only if propose messages were
- /// different.
- function duplicatePropose(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- uint256 _validRound1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- uint256 _validRound2,
- bytes memory _signature2
- ) internal pure returns (address) {
- require(
- !Compare.bytesEqual(_signature1, _signature2),
- "Validate: same signature"
- );
- address signer1 =
- recoverPropose(
- _height,
- _round,
- _blockhash1,
- _validRound1,
- _signature1
- );
- address signer2 =
- recoverPropose(
- _height,
- _round,
- _blockhash2,
- _validRound2,
- _signature2
- );
- require(signer1 == signer2, "Validate: different signer");
- return signer1;
- }
-
- function recoverPropose(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- uint256 _validRound,
- bytes memory _signature
- ) internal pure returns (address) {
- return
- ECDSA.recover(
- sha256(
- proposeMessage(_height, _round, _blockhash, _validRound)
- ),
- _signature
- );
- }
-
- function proposeMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- uint256 _validRound
- ) internal pure returns (bytes memory) {
- return
- abi.encodePacked(
- "Propose(Height=",
- String.fromUint(_height),
- ",Round=",
- String.fromUint(_round),
- ",BlockHash=",
- string(_blockhash),
- ",ValidRound=",
- String.fromUint(_validRound),
- ")"
- );
- }
-
- /// @notice Recovers two prevote messages and checks if they were signed by
- /// the same darknode. If they were different but the height and
- /// round were the same, then the darknode was behaving maliciously.
- /// @return The address of the signer if and only if prevote messages were
- /// different.
- function duplicatePrevote(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- bytes memory _signature2
- ) internal pure returns (address) {
- require(
- !Compare.bytesEqual(_signature1, _signature2),
- "Validate: same signature"
- );
- address signer1 =
- recoverPrevote(_height, _round, _blockhash1, _signature1);
- address signer2 =
- recoverPrevote(_height, _round, _blockhash2, _signature2);
- require(signer1 == signer2, "Validate: different signer");
- return signer1;
- }
-
- function recoverPrevote(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- bytes memory _signature
- ) internal pure returns (address) {
- return
- ECDSA.recover(
- sha256(prevoteMessage(_height, _round, _blockhash)),
- _signature
- );
- }
-
- function prevoteMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash
- ) internal pure returns (bytes memory) {
- return
- abi.encodePacked(
- "Prevote(Height=",
- String.fromUint(_height),
- ",Round=",
- String.fromUint(_round),
- ",BlockHash=",
- string(_blockhash),
- ")"
- );
- }
-
- /// @notice Recovers two precommit messages and checks if they were signed
- /// by the same darknode. If they were different but the height and
- /// round were the same, then the darknode was behaving maliciously.
- /// @return The address of the signer if and only if precommit messages were
- /// different.
- function duplicatePrecommit(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- bytes memory _signature2
- ) internal pure returns (address) {
- require(
- !Compare.bytesEqual(_signature1, _signature2),
- "Validate: same signature"
- );
- address signer1 =
- recoverPrecommit(_height, _round, _blockhash1, _signature1);
- address signer2 =
- recoverPrecommit(_height, _round, _blockhash2, _signature2);
- require(signer1 == signer2, "Validate: different signer");
- return signer1;
- }
-
- function recoverPrecommit(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- bytes memory _signature
- ) internal pure returns (address) {
- return
- ECDSA.recover(
- sha256(precommitMessage(_height, _round, _blockhash)),
- _signature
- );
- }
-
- function precommitMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash
- ) internal pure returns (bytes memory) {
- return
- abi.encodePacked(
- "Precommit(Height=",
- String.fromUint(_height),
- ",Round=",
- String.fromUint(_round),
- ",BlockHash=",
- string(_blockhash),
- ")"
- );
- }
-
- function recoverSecret(
- uint256 _a,
- uint256 _b,
- uint256 _c,
- uint256 _d,
- uint256 _e,
- uint256 _f,
- bytes memory _signature
- ) internal pure returns (address) {
- return
- ECDSA.recover(
- sha256(secretMessage(_a, _b, _c, _d, _e, _f)),
- _signature
- );
- }
-
- function secretMessage(
- uint256 _a,
- uint256 _b,
- uint256 _c,
- uint256 _d,
- uint256 _e,
- uint256 _f
- ) internal pure returns (bytes memory) {
- return
- abi.encodePacked(
- "Secret(",
- "ShamirShare(",
- String.fromUint(_a),
- ",",
- String.fromUint(_b),
- ",S256N(",
- String.fromUint(_c),
- "),",
- "S256PrivKey(",
- "S256N(",
- String.fromUint(_d),
- "),",
- "S256P(",
- String.fromUint(_e),
- "),",
- "S256P(",
- String.fromUint(_f),
- ")",
- ")",
- ")",
- ")"
- );
- }
-}
diff --git a/contracts/test/CompareTest.sol b/contracts/test/CompareTest.sol
deleted file mode 100644
index 7fbc65d1..00000000
--- a/contracts/test/CompareTest.sol
+++ /dev/null
@@ -1,14 +0,0 @@
-pragma solidity 0.5.17;
-
-import {Compare} from "../libraries/Compare.sol";
-
-/// @dev CompareTest exposes the internal functions of Compare.sol.
-contract CompareTest {
- function bytesEqual(bytes memory a, bytes memory b)
- public
- pure
- returns (bool)
- {
- return Compare.bytesEqual(a, b);
- }
-}
diff --git a/contracts/test/CycleChanger.sol b/contracts/test/CycleChanger.sol
deleted file mode 100644
index 85e4f57f..00000000
--- a/contracts/test/CycleChanger.sol
+++ /dev/null
@@ -1,23 +0,0 @@
-pragma solidity 0.5.17;
-
-import "../DarknodePayment/DarknodePayment.sol";
-
-/// @notice CycleChanger attempts to change the cycle twice in the same block.
-contract CycleChanger {
- DarknodePayment public darknodePayment; // Passed in as a constructor parameter.
-
- /// @notice The contract constructor.
- /// @param _darknodePayment The address of the DarknodePaymentStore contract.
- constructor(DarknodePayment _darknodePayment) public {
- darknodePayment = _darknodePayment;
- }
-
- function changeCycle() public {
- darknodePayment.changeCycle();
- darknodePayment.changeCycle();
- }
-
- function time() public view returns (uint256) {
- return block.timestamp;
- }
-}
diff --git a/contracts/test/ERC20WithFeesTest.sol b/contracts/test/ERC20WithFeesTest.sol
deleted file mode 100644
index 4fe25134..00000000
--- a/contracts/test/ERC20WithFeesTest.sol
+++ /dev/null
@@ -1,58 +0,0 @@
-pragma solidity 0.5.17;
-
-import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
-import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/SafeERC20.sol";
-
-import "../libraries/ERC20WithFees.sol";
-
-contract ERC20WithFeesTest {
- using SafeMath for uint256;
- using SafeERC20 for ERC20;
- using ERC20WithFees for ERC20;
-
- // Stores its own balance amount
- mapping(address => uint256) public balances;
-
- function deposit(address _token, uint256 _value) external {
- balances[_token] = ERC20(_token).balanceOf(address(this));
-
- uint256 newValue =
- ERC20(_token).safeTransferFromWithFees(
- msg.sender,
- address(this),
- _value
- );
- balances[_token] = balances[_token].add(newValue);
- require(
- ERC20(_token).balanceOf(address(this)) == balances[_token],
- "ERC20WithFeesTest: incorrect balance in deposit"
- );
- }
-
- function withdraw(address _token, uint256 _value) external {
- balances[_token] = ERC20(_token).balanceOf(address(this));
-
- ERC20(_token).safeTransfer(msg.sender, _value);
- balances[_token] = balances[_token].sub(_value);
- require(
- ERC20(_token).balanceOf(address(this)) == balances[_token],
- "ERC20WithFeesTest: incorrect balance in withdraw"
- );
- }
-
- function approve(address _token, uint256 _value) external {
- ERC20(_token).safeApprove(msg.sender, _value);
- }
-
- function naiveDeposit(address _token, uint256 _value) external {
- balances[_token] = ERC20(_token).balanceOf(address(this));
-
- ERC20(_token).safeTransferFrom(msg.sender, address(this), _value);
- balances[_token] = balances[_token].add(_value);
- require(
- ERC20(_token).balanceOf(address(this)) == balances[_token],
- "ERC20WithFeesTest: incorrect balance in deposit"
- );
- }
-}
diff --git a/contracts/test/StringTest.sol b/contracts/test/StringTest.sol
deleted file mode 100644
index a93ad75f..00000000
--- a/contracts/test/StringTest.sol
+++ /dev/null
@@ -1,27 +0,0 @@
-pragma solidity 0.5.17;
-
-import {String} from "../libraries/String.sol";
-
-/// @dev StringTest exposes the internal functions of String.sol.
-contract StringTest {
- function fromUint(uint256 _i) public pure returns (string memory) {
- return String.fromUint(_i);
- }
-
- function fromBytes32(bytes32 _value) public pure returns (string memory) {
- return String.fromBytes32(_value);
- }
-
- function fromAddress(address _addr) public pure returns (string memory) {
- return String.fromAddress(_addr);
- }
-
- function add4(
- string memory a,
- string memory b,
- string memory c,
- string memory d
- ) public pure returns (string memory) {
- return String.add8(a, b, c, d, "", "", "", "");
- }
-}
diff --git a/contracts/test/ValidateTest.sol b/contracts/test/ValidateTest.sol
deleted file mode 100644
index e1a194fb..00000000
--- a/contracts/test/ValidateTest.sol
+++ /dev/null
@@ -1,152 +0,0 @@
-pragma solidity 0.5.17;
-
-import "../libraries/Validate.sol";
-
-/// @notice Validate is a library for validating malicious darknode behaviour.
-contract ValidateTest {
- function duplicatePropose(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- uint256 _validRound1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- uint256 _validRound2,
- bytes memory _signature2
- ) public pure returns (address) {
- return
- Validate.duplicatePropose(
- _height,
- _round,
- _blockhash1,
- _validRound1,
- _signature1,
- _blockhash2,
- _validRound2,
- _signature2
- );
- }
-
- function recoverPropose(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- uint256 _validRound,
- bytes memory _signature
- ) public pure returns (address) {
- return
- Validate.recoverPropose(
- _height,
- _round,
- _blockhash,
- _validRound,
- _signature
- );
- }
-
- function duplicatePrevote(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- bytes memory _signature2
- ) public pure returns (address) {
- return
- Validate.duplicatePrevote(
- _height,
- _round,
- _blockhash1,
- _signature1,
- _blockhash2,
- _signature2
- );
- }
-
- function recoverPrevote(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- bytes memory _signature
- ) public pure returns (address) {
- return Validate.recoverPrevote(_height, _round, _blockhash, _signature);
- }
-
- function duplicatePrecommit(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash1,
- bytes memory _signature1,
- bytes memory _blockhash2,
- bytes memory _signature2
- ) public pure returns (address) {
- return
- Validate.duplicatePrecommit(
- _height,
- _round,
- _blockhash1,
- _signature1,
- _blockhash2,
- _signature2
- );
- }
-
- function recoverPrecommit(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- bytes memory _signature
- ) public pure returns (address) {
- return
- Validate.recoverPrecommit(_height, _round, _blockhash, _signature);
- }
-
- function proposeMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash,
- uint256 _validRound
- ) public pure returns (bytes memory) {
- return
- Validate.proposeMessage(_height, _round, _blockhash, _validRound);
- }
-
- function prevoteMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash
- ) public pure returns (bytes memory) {
- return Validate.prevoteMessage(_height, _round, _blockhash);
- }
-
- function precommitMessage(
- uint256 _height,
- uint256 _round,
- bytes memory _blockhash
- ) public pure returns (bytes memory) {
- return Validate.precommitMessage(_height, _round, _blockhash);
- }
-
- function recoverSecret(
- uint256 _a,
- uint256 _b,
- uint256 _c,
- uint256 _d,
- uint256 _e,
- uint256 _f,
- bytes memory _signature
- ) public pure returns (address) {
- return Validate.recoverSecret(_a, _b, _c, _d, _e, _f, _signature);
- }
-
- function secretMessage(
- uint256 _a,
- uint256 _b,
- uint256 _c,
- uint256 _d,
- uint256 _e,
- uint256 _f
- ) public pure returns (bytes memory) {
- return Validate.secretMessage(_a, _b, _c, _d, _e, _f);
- }
-}
diff --git a/migrations/1_darknodes.js b/migrations/1_darknodes.js
index 54016bea..b53f1fe2 100644
--- a/migrations/1_darknodes.js
+++ b/migrations/1_darknodes.js
@@ -1,19 +1,14 @@
///
-const BN = require("bn.js");
const { execSync } = require("child_process");
const RenToken = artifacts.require("RenToken");
-const DarknodePayment = artifacts.require("DarknodePayment");
-const DarknodePaymentStore = artifacts.require("DarknodePaymentStore");
-const ClaimlessRewards = artifacts.require("ClaimlessRewards");
const DarknodeRegistryStore = artifacts.require("DarknodeRegistryStore");
const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const DarknodeSlasher = artifacts.require("DarknodeSlasher");
-const Protocol = artifacts.require("Protocol");
-const ClaimRewards = artifacts.require("ClaimRewards");
-const GetOperatorDarknodes = artifacts.require("GetOperatorDarknodes");
+const DarknodeRegistryLogicV2 = artifacts.require("DarknodeRegistryLogicV2");
+const DarknodeRegistryV1ToV2Upgrader = artifacts.require(
+ "DarknodeRegistryV1ToV2Upgrader"
+);
const RenProxyAdmin = artifacts.require("RenProxyAdmin");
const networks = require("./networks.js");
@@ -23,9 +18,7 @@ const { encodeCallData } = require("./encode");
const NULL = "0x0000000000000000000000000000000000000000";
const gitCommit = () =>
- execSync("git describe --always --long")
- .toString()
- .trim();
+ execSync("git describe --always --long").toString().trim();
/**
* @dev In order to specify what contracts to re-deploy, update `networks.js`.
@@ -40,7 +33,7 @@ const gitCommit = () =>
* @param {any} deployer
* @param {string} network
*/
-module.exports = async function(deployer, network) {
+module.exports = async function (deployer, network) {
const contractOwner = (await web3.eth.getAccounts())[0];
const Ox = web3.utils.toChecksumAddress;
@@ -58,35 +51,16 @@ module.exports = async function(deployer, network) {
const VERSION_STRING = `${network}-${gitCommit()}`;
RenToken.address = addresses.RenToken || "";
- DarknodeSlasher.address = addresses.DarknodeSlasher || "";
DarknodeRegistryProxy.address = addresses.DarknodeRegistryProxy || "";
- DarknodeRegistryLogicV1.address = addresses.DarknodeRegistryLogicV1 || "";
+ DarknodeRegistryLogicV2.address = addresses.DarknodeRegistryLogicV2 || "";
DarknodeRegistryStore.address = addresses.DarknodeRegistryStore || "";
- DarknodePaymentStore.address = addresses.DarknodePaymentStore || "";
- DarknodePayment.address = addresses.DarknodePayment || "";
- ClaimlessRewards.address = addresses.ClaimlessRewards || "";
- Protocol.address = addresses.Protocol || "";
RenProxyAdmin.address = addresses.RenProxyAdmin || "";
- GetOperatorDarknodes.address = addresses.GetOperatorDarknodes || "";
- ClaimRewards.address = addresses.ClaimRewards || "";
- const tokens = addresses.tokens || {};
+ DarknodeRegistryV1ToV2Upgrader.address =
+ addresses.DarknodeRegistryV1ToV2Upgrader || "";
- let actionCount = 0;
-
- /** GetOperatorDarknodes **************************************************************/
+ const slasher = NULL;
- // !!! 0x4e27a3e21e747cf875ad5829b6d9cb7700b8b5f0
- // if (!GetOperatorDarknodes.address) {
- // deployer.logger.log("Deploying GetOperatorDarknodes");
- // await deployer.deploy(
- // GetOperatorDarknodes,
- // DarknodeRegistryProxy.address
- // );
- // actionCount++;
- // }
- // const getOperatorDarknodes = await GetOperatorDarknodes.at(
- // GetOperatorDarknodes.address
- // );
+ let actionCount = 0;
/** PROXY ADMIN ***********************************************************/
if (!RenProxyAdmin.address) {
@@ -96,25 +70,6 @@ module.exports = async function(deployer, network) {
}
let renProxyAdmin = await RenProxyAdmin.at(RenProxyAdmin.address);
- // /** GetOperatorDarknodes **************************************************************/
- // if (!GetOperatorDarknodes.address) {
- // deployer.logger.log("Deploying GetOperatorDarknodes");
- // await deployer.deploy(GetOperatorDarknodes);
- // actionCount++;
- // }
- // const getOperatorDarknodes = await GetOperatorDarknodes.at(
- // GetOperatorDarknodes.address
- // );
-
- /** PROTOCOL **************************************************************/
- if (!Protocol.address) {
- deployer.logger.log("Deploying Protocol");
- await deployer.deploy(Protocol);
- actionCount++;
- }
- const protocol = await Protocol.at(Protocol.address);
- await protocol.__Protocol_init(contractOwner);
-
/** Ren TOKEN *************************************************************/
if (!RenToken.address) {
deployer.logger.log("Deploying RenToken");
@@ -122,14 +77,6 @@ module.exports = async function(deployer, network) {
actionCount++;
}
- /** ClaimRewards **************************************************************/
- if (!ClaimRewards.address) {
- deployer.logger.log("Deploying ClaimRewards");
- await deployer.deploy(ClaimRewards);
- actionCount++;
- }
- // const claimRewards = await ClaimRewards.at(ClaimRewards.address);
-
/** DARKNODE REGISTRY *****************************************************/
if (!DarknodeRegistryStore.address) {
deployer.logger.log("Deploying DarknodeRegistryStore");
@@ -144,12 +91,12 @@ module.exports = async function(deployer, network) {
DarknodeRegistryStore.address
);
- if (!DarknodeRegistryLogicV1.address) {
- deployer.logger.log("Deploying DarknodeRegistryLogicV1");
- await deployer.deploy(DarknodeRegistryLogicV1);
+ if (!DarknodeRegistryLogicV2.address) {
+ deployer.logger.log("Deploying DarknodeRegistryLogicV2");
+ await deployer.deploy(DarknodeRegistryLogicV2);
}
- const darknodeRegistryLogic = await DarknodeRegistryLogicV1.at(
- DarknodeRegistryLogicV1.address
+ const darknodeRegistryLogic = await DarknodeRegistryLogicV2.at(
+ DarknodeRegistryLogicV2.address
);
const darknodeRegistryParameters = {
types: [
@@ -159,7 +106,7 @@ module.exports = async function(deployer, network) {
"uint256",
"uint256",
"uint256",
- "uint256"
+ "uint256",
],
values: [
VERSION_STRING,
@@ -168,8 +115,8 @@ module.exports = async function(deployer, network) {
config.MINIMUM_BOND.toString(),
config.MINIMUM_POD_SIZE,
config.MINIMUM_EPOCH_INTERVAL_SECONDS,
- 0
- ]
+ 0,
+ ],
};
// Initialize darknodeRegistryLogic so others can't initialize it.
@@ -211,13 +158,14 @@ module.exports = async function(deployer, network) {
DarknodeRegistryProxy.address
);
}
- const darknodeRegistry = await DarknodeRegistryLogicV1.at(
+ const darknodeRegistry = await DarknodeRegistryLogicV2.at(
DarknodeRegistryProxy.address
);
- const darknodeRegistryProxyLogic = await renProxyAdmin.getProxyImplementation(
- darknodeRegistryProxy.address
- );
+ const darknodeRegistryProxyLogic =
+ await renProxyAdmin.getProxyImplementation(
+ darknodeRegistryProxy.address
+ );
if (Ox(darknodeRegistryProxyLogic) !== Ox(darknodeRegistryLogic.address)) {
deployer.logger.log(
`DarknodeRegistryProxy is pointing to out-dated ProtocolLogic. Was ${Ox(
@@ -255,7 +203,7 @@ module.exports = async function(deployer, network) {
deployer.logger.log(
`Transferring DNRS ownership from ${storeOwner} to new DNR`
);
- const oldDNR = await DarknodeRegistryLogicV1.at(storeOwner);
+ const oldDNR = await DarknodeRegistryLogicV2.at(storeOwner);
oldDNR.transferStoreOwnership(darknodeRegistry.address);
// This will also call claim, but we try anyway because older
// contracts didn't:
@@ -269,20 +217,6 @@ module.exports = async function(deployer, network) {
actionCount++;
}
- const protocolDarknodeRegistry = await protocol.getContract(
- "DarknodeRegistry"
- );
- if (Ox(protocolDarknodeRegistry) !== Ox(darknodeRegistry.address)) {
- deployer.logger.log(
- `Updating DarknodeRegistry in Protocol contract. Was ${protocolDarknodeRegistry}, now is ${darknodeRegistry.address}`
- );
- await protocol.updateContract(
- "DarknodeRegistry",
- darknodeRegistry.address
- );
- actionCount++;
- }
-
const renInDNR = await darknodeRegistry.ren();
if (Ox(renInDNR) !== Ox(RenToken.address)) {
console.error(
@@ -305,216 +239,33 @@ module.exports = async function(deployer, network) {
);
}
- /***************************************************************************
- ** SLASHER ****************************************************************
- **************************************************************************/
- if (!DarknodeSlasher.address) {
- deployer.logger.log("Deploying DarknodeSlasher");
- await deployer.deploy(DarknodeSlasher, darknodeRegistry.address);
- actionCount++;
- }
- const slasher = await DarknodeSlasher.at(DarknodeSlasher.address);
-
- const dnrInSlasher = await slasher.darknodeRegistry();
- if (Ox(dnrInSlasher) !== Ox(darknodeRegistry.address)) {
- deployer.logger.log("Updating DNR in Slasher");
- await slasher.updateDarknodeRegistry(darknodeRegistry.address);
- actionCount++;
- }
-
- // Set the slash percentages
- const blacklistSlashPercent = new BN(
- await slasher.blacklistSlashPercent()
- ).toNumber();
- if (blacklistSlashPercent !== config.BLACKLIST_SLASH_PERCENT) {
- deployer.logger.log("Setting blacklist slash percent");
- await slasher.setBlacklistSlashPercent(
- new BN(config.BLACKLIST_SLASH_PERCENT)
- );
- actionCount++;
- }
- const maliciousSlashPercent = new BN(
- await slasher.maliciousSlashPercent()
- ).toNumber();
- if (maliciousSlashPercent !== config.MALICIOUS_SLASH_PERCENT) {
- deployer.logger.log("Setting malicious slash percent");
- await slasher.setMaliciousSlashPercent(
- new BN(config.MALICIOUS_SLASH_PERCENT)
- );
- actionCount++;
- }
- const secretRevealSlashPercent = new BN(
- await slasher.secretRevealSlashPercent()
- ).toNumber();
- if (secretRevealSlashPercent !== config.SECRET_REVEAL_SLASH_PERCENT) {
- deployer.logger.log("Setting secret reveal slash percent");
- await slasher.setSecretRevealSlashPercent(
- new BN(config.SECRET_REVEAL_SLASH_PERCENT)
- );
- actionCount++;
- }
-
- const currentSlasher = await darknodeRegistry.slasher();
- const nextSlasher = await darknodeRegistry.nextSlasher();
- if (
- Ox(currentSlasher) != Ox(DarknodeSlasher.address) &&
- Ox(nextSlasher) != Ox(DarknodeSlasher.address)
- ) {
- deployer.logger.log("Linking DarknodeSlasher and DarknodeRegistry");
- // Update slasher address
- await darknodeRegistry.updateSlasher(DarknodeSlasher.address);
- actionCount++;
- }
-
- /***************************************************************************
- ** DARKNODE PAYMENT *******************************************************
- **************************************************************************/
- if (!DarknodePaymentStore.address) {
- deployer.logger.log("Deploying DarknodePaymentStore");
- await deployer.deploy(DarknodePaymentStore, VERSION_STRING);
- actionCount++;
- }
-
- if (!DarknodePayment.address) {
- // Deploy Darknode Payment
- deployer.logger.log("Deploying DarknodePayment");
- await deployer.deploy(
- DarknodePayment,
- VERSION_STRING,
- darknodeRegistry.address,
- DarknodePaymentStore.address,
- config.DARKNODE_PAYOUT_PERCENT // Reward payout percentage (50% is paid out at any given cycle)
- );
- actionCount++;
- }
+ // const currentSlasher = await darknodeRegistry.slasher();
+ // const nextSlasher = await darknodeRegistry.nextSlasher();
+ // if (Ox(currentSlasher) != Ox(slasher) && Ox(nextSlasher) != Ox(slasher)) {
+ // deployer.logger.log("Linking DarknodeSlasher and DarknodeRegistry");
+ // // Update slasher address
+ // await darknodeRegistry.updateSlasher(slasher);
+ // actionCount++;
+ // }
- if (!ClaimlessRewards.address) {
- // Deploy Darknode Payment
- deployer.logger.log("Deploying ClaimlessRewards");
+ if (!DarknodeRegistryV1ToV2Upgrader.address) {
await deployer.deploy(
- ClaimlessRewards,
+ DarknodeRegistryV1ToV2Upgrader,
+ renProxyAdmin.address,
darknodeRegistry.address,
- DarknodePaymentStore.address,
- config.communityFund || contractOwner,
- config.communityFundNumerator || 50000
+ darknodeRegistryLogic.address
);
actionCount++;
}
- // Update darknode payment address
- if (
- Ox(await darknodeRegistry.darknodePayment()) !==
- Ox(DarknodePayment.address)
- ) {
- deployer.logger.log("Updating DarknodeRegistry's darknode payment");
- await darknodeRegistry.updateDarknodePayment(DarknodePayment.address);
- actionCount++;
- }
-
- const darknodePayment = await DarknodePayment.at(DarknodePayment.address);
- for (const tokenName of Object.keys(tokens)) {
- const tokenAddress = tokens[tokenName];
- const registered =
- (
- await darknodePayment.registeredTokenIndex(tokenAddress)
- ).toString() !== "0";
- const pendingRegistration = await darknodePayment.tokenPendingRegistration(
- tokenAddress
- );
- if (!registered && !pendingRegistration) {
- deployer.logger.log(
- `Registering token ${tokenName} in DarknodePayment`
- );
- await darknodePayment.registerToken(tokenAddress);
- actionCount++;
- }
- }
-
- const dnrInDarknodePayment = await darknodePayment.darknodeRegistry();
- if (Ox(dnrInDarknodePayment) !== Ox(darknodeRegistry.address)) {
- deployer.logger.log("DNP is still pointing to Forwarder.");
-
- // deployer.logger.log("Updating DNR in DNP");
- // await darknodePayment.updateDarknodeRegistry(darknodeRegistry.address);
- // actionCount++;
- }
-
- const darknodePaymentStore = await DarknodePaymentStore.at(
- DarknodePaymentStore.address
- );
- const currentOwner = await darknodePaymentStore.owner();
- if (Ox(currentOwner) !== Ox(DarknodePayment.address)) {
- deployer.logger.log("Linking DarknodePaymentStore and DarknodePayment");
-
- if (currentOwner === contractOwner) {
- await darknodePaymentStore.transferOwnership(
- DarknodePayment.address
- );
-
- // Update DarknodePaymentStore address
- deployer.logger.log(`Claiming DNPS ownership in DNP`);
- await darknodePayment.claimStoreOwnership();
- } else {
- deployer.logger.log(
- `Transferring DNPS ownership from ${currentOwner} to new DNP`
- );
- const oldDarknodePayment = await DarknodePayment.at(currentOwner);
- await oldDarknodePayment.transferStoreOwnership(
- DarknodePayment.address
- );
- // This will also call claim, but we try anyway because older
- // contracts didn't:
- try {
- // Claim ownership
- await darknodePayment.claimStoreOwnership();
- } catch (error) {
- // Ignore
- }
- }
- actionCount++;
- }
-
- // if (changeCycle) {
- // try {
- // deployer.logger.log("Attempting to change cycle");
- // await darknodePayment.changeCycle();
- // } catch (error) {
- // deployer.logger.log("Unable to call darknodePayment.changeCycle()");
- // }
- // }
-
- // Set the darknode payment cycle changer to the darknode registry
- if (
- Ox(await darknodePayment.cycleChanger()) !==
- Ox(darknodeRegistry.address)
- ) {
- deployer.logger.log("Setting the DarknodePayment's cycle changer");
- await darknodePayment.updateCycleChanger(darknodeRegistry.address);
- actionCount++;
- }
-
deployer.logger.log(`Performed ${actionCount} updates.`);
deployer.logger.log(`
-
- /* 1_darknodes.js */
-
RenProxyAdmin: "${RenProxyAdmin.address}",
RenToken: "${RenToken.address}",
-
- // Protocol
- Protocol: "${Protocol.address}",
-
- // DNR
DarknodeRegistryStore: "${DarknodeRegistryStore.address}",
- DarknodeRegistryLogicV1: "${DarknodeRegistryLogicV1.address}",
+ DarknodeRegistryLogicV2: "${DarknodeRegistryLogicV2.address}",
DarknodeRegistryProxy: "${DarknodeRegistryProxy.address}",
-
- // DNP
- DarknodePaymentStore: "${DarknodePaymentStore.address}",
- DarknodePayment: "${DarknodePayment.address}",
-
- // Slasher
- DarknodeSlasher: "${DarknodeSlasher.address}",
+ DarknodeRegistryV1ToV2Upgrader: "${DarknodeRegistryV1ToV2Upgrader.address}",
`);
};
diff --git a/migrations/networks.js b/migrations/networks.js
index 7ad4db8c..08b87fb9 100644
--- a/migrations/networks.js
+++ b/migrations/networks.js
@@ -7,7 +7,7 @@ const config = {
DARKNODE_PAYOUT_PERCENT: 50, // Only payout 50% of the reward pool
BLACKLIST_SLASH_PERCENT: 0, // Don't slash bond for blacklisting
MALICIOUS_SLASH_PERCENT: 50, // Slash 50% of the bond
- SECRET_REVEAL_SLASH_PERCENT: 100 // Slash 100% of the bond
+ SECRET_REVEAL_SLASH_PERCENT: 100, // Slash 100% of the bond
};
module.exports = {
@@ -24,6 +24,8 @@ module.exports = {
DarknodeRegistryStore: "0x60Ab11FE605D2A2C3cf351824816772a131f8782",
DarknodeRegistryLogicV1: "0x33b53A700de61b6be01d65A758b3635584bCF140",
DarknodeRegistryProxy: "0x2D7b6C95aFeFFa50C068D50f89C5C0014e054f0A",
+ DarknodeRegistryLogicV2: "",
+ DarknodeRegistryV1ToV2Upgrader: "",
// DNP
DarknodePaymentStore: "0xE33417797d6b8Aec9171d0d6516E88002fbe23E7",
@@ -31,12 +33,12 @@ module.exports = {
tokens: {
DAI: "0x6b175474e89094c44da98b954eedeac495271d0f",
- ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
+ ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
},
config: {
- ...config
- }
+ ...config,
+ },
},
testnet: {
RenProxyAdmin: "0x4C695C4Aa6238f0A7092733180328c2E64C912C7",
@@ -50,7 +52,10 @@ module.exports = {
// DNR
DarknodeRegistryStore: "0x9daa16aA19e37f3de06197a8B5E638EC5e487392",
DarknodeRegistryLogicV1: "0x046EDe9916e13De79d5530b67FF5dEbB7B72742C",
+ DarknodeRegistryLogicV2: "0x61ffD5059Af59D480C57d43DCC09eea653e95eC8",
DarknodeRegistryProxy: "0x9954C9F839b31E82bc9CA98F234313112D269712",
+ DarknodeRegistryV1ToV2Upgrader:
+ "0x6587720afB2b306b1888408B907E2A4DD8B18651",
// DNP
DarknodePaymentStore: "0x0EC73cCDCd8e643d909D0c4b663Eb1B2Fb0b1e1C",
@@ -58,12 +63,12 @@ module.exports = {
tokens: {
DAI: "0xc4375b7de8af5a38a93548eb8453a498222c4ff2",
- ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
+ ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
},
config: {
- ...config
- }
+ ...config,
+ },
},
devnet: {
@@ -79,6 +84,8 @@ module.exports = {
DarknodeRegistryStore: "0x3ccF0cd02ff15b59Ce2B152CdDE78551eFd34a62",
DarknodeRegistryLogicV1: "0x26D6fEC1C904EB5b86ACed6BB804b4ed35208704",
DarknodeRegistryProxy: "0x7B69e5e15D4c24c353Fea56f72E4C0c5B93dCb71",
+ DarknodeRegistryLogicV2: "",
+ DarknodeRegistryV1ToV2Upgrader: "",
// DNP
DarknodePaymentStore: "0xfb98D6900330844CeAce6Ae4ae966D272bE1aeC3",
@@ -86,13 +93,13 @@ module.exports = {
tokens: {
DAI: "0xc4375b7de8af5a38a93548eb8453a498222c4ff2",
- ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
+ ETH: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
},
config: {
- ...config
- }
+ ...config,
+ },
},
- config
+ config,
};
diff --git a/test/ClaimlessRewards/ClaimlessRewards.ts b/test/ClaimlessRewards/ClaimlessRewards.ts
deleted file mode 100644
index 2fa68907..00000000
--- a/test/ClaimlessRewards/ClaimlessRewards.ts
+++ /dev/null
@@ -1,1217 +0,0 @@
-import BigNumber from "bignumber.js";
-import BN from "bn.js";
-import seedrandom from "seedrandom";
-
-import {
- ClaimlessRewardsInstance,
- DarknodePaymentInstance,
- DarknodePaymentStoreInstance,
- DarknodeRegistryLogicV1Instance,
- DarknodeSlasherInstance,
- PaymentTokenInstance,
- RenTokenInstance,
-} from "../../types/truffle-contracts";
-import {
- DAYS,
- ETHEREUM,
- getDecimals,
- HOURS,
- ID,
- increaseTime,
- MINIMUM_BOND,
- NULL,
- PUBK,
- range,
- toBN,
- waitForEpoch,
-} from "../helper/testUtils";
-import { STEPS } from "./steps";
-
-const RenToken = artifacts.require("RenToken");
-const ERC20 = artifacts.require("PaymentToken");
-const DarknodePaymentStore = artifacts.require("DarknodePaymentStore");
-const ClaimlessRewards = artifacts.require("ClaimlessRewards");
-const DarknodePayment = artifacts.require("DarknodePayment");
-const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const SelfDestructingToken = artifacts.require("SelfDestructingToken");
-const DarknodeSlasher = artifacts.require("DarknodeSlasher");
-
-contract("ClaimlessRewards", (accounts: string[]) => {
- let store: DarknodePaymentStoreInstance;
- let dai: PaymentTokenInstance;
- let erc20Token: PaymentTokenInstance;
- let dnr: DarknodeRegistryLogicV1Instance;
- let rewards: ClaimlessRewardsInstance;
- let ren: RenTokenInstance;
- let slasher: DarknodeSlasherInstance;
- let dnp: DarknodePaymentInstance;
-
- const owner = accounts[0];
- const operator1 = accounts[1];
- const operator2 = accounts[2];
-
- before(async () => {
- ren = await RenToken.deployed();
- dai = await ERC20.new("DAI");
- erc20Token = await ERC20.new("ERC20");
- const dnrProxy = await DarknodeRegistryProxy.deployed();
- dnr = await DarknodeRegistryLogicV1.at(dnrProxy.address);
- store = await DarknodePaymentStore.deployed();
- rewards = await ClaimlessRewards.deployed();
- dnp = await DarknodePayment.deployed();
- slasher = await DarknodeSlasher.deployed();
- await dnr.updateSlasher(slasher.address);
-
- await dnp.transferStoreOwnership(rewards.address);
- await dnr.updateDarknodePayment(rewards.address);
- await dnr.updateMinimumEpochInterval(60 * 60);
- await STEPS.waitForEpoch(rewards);
-
- new BN(await dnr.numDarknodes()).should.bignumber.equal(new BN(0));
- });
-
- after(async () => {
- await rewards.transferStoreOwnership(dnp.address);
- await dnr.updateDarknodePayment(dnp.address);
- await dnr.updateMinimumEpochInterval(30);
- });
-
- afterEach(async () => {
- // Deregister tokens.
- const tokens = await rewards.getRegisteredTokens();
- for (const token of tokens) {
- await rewards.deregisterToken(token);
- }
-
- await STEPS.waitForEpoch(rewards);
-
- // Deregister darknodes.
- const darknodes = await dnr.getDarknodes(NULL, 0);
- if (darknodes.length) {
- for (const darknode of darknodes) {
- await dnr.deregister(darknode, {
- from: await dnr.getDarknodeOperator(darknode),
- });
- }
-
- await STEPS.waitForEpoch(rewards);
-
- await STEPS.waitForEpoch(rewards);
-
- for (const darknode of darknodes) {
- await dnr.refund(darknode, {
- from: await dnr.getDarknodeOperator(darknode),
- });
- }
- }
- });
-
- describe("Token registration", async () => {
- it("cannot register token if not owner", async () => {
- await rewards
- .registerToken(dai.address, { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- });
-
- it("can register token", async () => {
- // No tokens should be registered.
- (await rewards.getRegisteredTokens()).length.should.equal(0);
-
- await STEPS.registerToken(rewards, dai.address);
- await STEPS.registerToken(rewards, erc20Token.address);
- await STEPS.registerToken(rewards, ETHEREUM);
-
- (await rewards.getRegisteredTokens()).length.should.equal(3);
- });
-
- it("cannot register already registered tokens", async () => {
- await STEPS.registerToken(rewards, dai.address);
- await rewards
- .registerToken(dai.address)
- .should.be.rejectedWith(
- /ClaimlessRewards: token already registered/
- );
- });
-
- it("cannot deregister token if not owner", async () => {
- await STEPS.registerToken(rewards, ETHEREUM);
- await rewards
- .deregisterToken(ETHEREUM, { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- });
-
- it("can deregister tokens", async () => {
- await STEPS.registerToken(rewards, dai.address);
- await STEPS.registerToken(rewards, erc20Token.address);
- await STEPS.registerToken(rewards, ETHEREUM);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await rewards
- .deregisterToken(ETHEREUM)
- .should.be.rejectedWith(
- /ClaimlessRewards: token not registered/
- );
- await STEPS.deregisterToken(rewards, erc20Token.address);
- await STEPS.deregisterToken(rewards, dai.address);
-
- await STEPS.registerToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, ETHEREUM);
- });
-
- it("can deregister a destroyed token", async () => {
- await registerNode(6);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
-
- // Register token.
- const sdt = await SelfDestructingToken.new();
- await STEPS.registerToken(rewards, sdt.address);
- await STEPS.waitForEpoch(rewards);
-
- // Self destruct token.
- await sdt.destruct();
- await STEPS.deregisterToken(rewards, sdt.address);
-
- await deregisterNode(6);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode(6);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("cannot deregister unregistered tokens", async () => {
- await rewards
- .deregisterToken(ETHEREUM)
- .should.be.rejectedWith(
- /ClaimlessRewards: token not registered/
- );
- });
- });
-
- describe("Token deposits", async () => {
- it("can deposit ETH via direct payment to DarknodePaymentStore contract", async () => {
- // deposit using direct deposit to store
- const oldETHBalance = new BN(await store.totalBalance(ETHEREUM));
- const oldFreeBalance = new BN(
- await store.availableBalance(ETHEREUM)
- );
- const amount = new BN(1).mul(new BN(10).pow(new BN(18)));
- await web3.eth.sendTransaction({
- to: store.address,
- from: owner,
- value: amount.toString(),
- });
- // Total balance has increased.
- new BN(await store.totalBalance(ETHEREUM)).should.bignumber.equal(
- oldETHBalance.add(amount)
- );
- // Reward pool has increased.
- new BN(
- await store.availableBalance(ETHEREUM)
- ).should.bignumber.equal(oldFreeBalance.add(amount));
- });
- });
-
- describe("Claiming rewards", async () => {
- it("nodes can earn ETH", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, ETHEREUM);
- await STEPS.waitForEpoch(rewards);
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- // We should have zero claimed balance before ticking
- (
- await rewards.darknodeBalances(ID(1), ETHEREUM)
- ).should.bignumber.equal(0);
-
- // Change cycle after 1 month.
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- const node1Amount1 = await STEPS.withdraw(
- rewards,
- ID(1),
- ETHEREUM,
- operator1
- );
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- const node1Amount2 = await STEPS.withdraw(
- rewards,
- ID(1),
- ETHEREUM,
- operator1
- );
-
- const node2Amount1 = await STEPS.withdraw(
- rewards,
- ID(2),
- ETHEREUM,
- operator2
- );
-
- node2Amount1.should.bignumber.equal(
- node1Amount1.plus(node1Amount2)
- );
-
- await STEPS.changeCycle(rewards, 1 * HOURS);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
-
- await STEPS.changeCycle(rewards, 1 * HOURS);
-
- // Can still withdraw owed ETH rewards after deregistration.
- (
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1)
- ).should.bignumber.greaterThan(0);
- await STEPS.withdraw(rewards, ID(2), ETHEREUM, operator2);
-
- await STEPS.changeCycle(rewards, 1 * HOURS);
-
- // No more ETH rewards to withdraw.
- (
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1)
- ).should.bignumber.equal(0);
-
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("nodes can earn DAI", async () => {
- // register ETH token and two nodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- await STEPS.changeCycle(rewards, 1 * HOURS);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
- await STEPS.withdraw(rewards, ID(2), dai.address, operator2);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("nodes can earn ETH and DAI", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await STEPS.withdraw(
- rewards,
- ID(1),
- [dai.address, ETHEREUM],
- operator1
- );
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("node can withdraw after deregistering", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, ETHEREUM);
- await STEPS.waitForEpoch(rewards);
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- // Change cycle after 1 month.
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- // Check that deregistering doesn't affect withdrawable balance.
-
- const node1BalanceBefore = await toBN(
- rewards.darknodeBalances(ID(1), ETHEREUM)
- );
- const node2BalanceBefore = await toBN(
- rewards.darknodeBalances(ID(2), ETHEREUM)
- );
- node1BalanceBefore.should.bignumber.equal(node2BalanceBefore);
-
- await deregisterNode(1);
-
- const node1BalanceAfter = await toBN(
- rewards.darknodeBalances(ID(1), ETHEREUM)
- );
- const node2BalanceAfter = await toBN(
- rewards.darknodeBalances(ID(2), ETHEREUM)
- );
- node1BalanceAfter.should.bignumber.equal(node2BalanceAfter);
-
- (
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1)
- ).should.bignumber.greaterThan(0);
-
- await STEPS.waitForEpoch(rewards);
-
- // The node can withdraw its rewards from it's last epoch.
- (
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1)
- ).should.bignumber.greaterThan(0);
-
- await STEPS.waitForEpoch(rewards);
-
- // The node should no longer be earning rewards.
- (
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1)
- ).should.bignumber.equal(0);
- await STEPS.waitForEpoch(rewards);
-
- await refundNode(1);
-
- await STEPS.withdraw(
- rewards,
- operator1,
- ETHEREUM
- ).should.be.rejectedWith(/ClaimlessRewards: not operator/);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await deregisterNode(2);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode(2);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("can withdraw after re-registering", async () => {
- // register ETH token and two darknodes
- await registerNode(1);
- await STEPS.registerToken(rewards, ETHEREUM);
- await STEPS.waitForEpoch(rewards);
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- await deregisterNode(1);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
-
- // Ensure all rewards have been withdrawn.
- await STEPS.withdraw(rewards, ID(1), ETHEREUM, operator1);
-
- await refundNode(1);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await registerNode([1, 2]);
-
- await STEPS.waitForEpoch(rewards);
-
- const node1Amount = await STEPS.withdraw(
- rewards,
- ID(1),
- ETHEREUM,
- operator1
- );
- const node2Amount = await STEPS.withdraw(
- rewards,
- ID(2),
- ETHEREUM,
- operator2
- );
-
- // node1 should not be able to withdraw additional rewards,
- // since it re-registered at the same time as node2.
- node2Amount.should.bignumber.equal(node1Amount);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("calling cycle immediately will not add new timestamp", async () => {
- await STEPS.changeCycle(rewards, 1 * HOURS);
- await STEPS.changeCycle(rewards, 0 * HOURS).should.be.rejectedWith(
- /ClaimlessRewards: previous cycle too recent/
- );
- });
-
- it("epoch can progress even if cycle is too recent", async () => {
- const timeout = new BN(
- (await dnr.minimumEpochInterval()).toString()
- ).toNumber();
-
- await increaseTime(Math.max(timeout, 1 * HOURS));
-
- await STEPS.changeCycle(rewards, 0);
- await dnr.epoch();
- });
-
- it("can withdraw for multiple nodes", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2], operator1);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- // Withdraw DAI for second nodes.
- await STEPS.withdraw(rewards, [ID(2)], dai.address, operator1);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- // Withdraw DAI and ETH for both nodes.
- await STEPS.withdraw(
- rewards,
- [ID(1), ID(2)],
- [dai.address, ETHEREUM],
- operator1
- );
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2], operator1);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2], operator1);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("only operator can withdraw", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- await STEPS.withdraw(
- rewards,
- ID(1),
- dai.address,
- operator2
- ).should.be.rejectedWith(/ClaimlessRewards: not operator/);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
- await STEPS.withdraw(rewards, ID(2), dai.address, operator2);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("nodes can withdraw after migrating from DarknodePayment contract", async function () {
- // Requires Darknode Registry implementation to be upgraded mid-test.
-
- this.timeout(100 * 300000);
-
- const seed = 0.4957910354433912; // 20
- // const seed = 0.12399667580170748; // 4
- // const seed = Math.random();
- console.log(`Starting test with seed ${seed}.`);
- const rng = seedrandom(seed.toString());
-
- await rewards.transferStoreOwnership(dnp.address);
- await dnr.updateDarknodePayment(dnp.address);
-
- const newToken = await ERC20.new("DAI2");
- await dnp.registerToken(newToken.address);
-
- (
- await store.darknodeBalances(NULL, newToken.address)
- ).should.bignumber.equal(0);
-
- const darknodeIndices = range(20);
-
- // Register 50 nodes before switching to ClaimlessRewards.
- let previousCanClaim = 0;
- for (const index of darknodeIndices.slice(
- 0,
- Math.floor(darknodeIndices.length / 2)
- )) {
- await registerNode(index);
-
- if (rng() < 0.5) {
- await waitForEpoch(dnr);
-
- // Claim for any darknode that has been registered for two
- // epochs.
- for (const indexInner of darknodeIndices.slice(
- 0,
- previousCanClaim
- )) {
- await dnp.claim(ID(indexInner));
- }
-
- previousCanClaim = index;
-
- // Add random amount of DAI to rewards.
- await STEPS.addRewards(
- dnp,
- newToken.address,
- new BigNumber(rng())
- .times(1000)
- .times(
- new BigNumber(10).exponentiatedBy(
- await getDecimals(newToken.address)
- )
- )
- );
- }
- }
-
- await waitForEpoch(dnr);
- await waitForEpoch(dnr);
- for (const index of darknodeIndices.slice(
- 0,
- Math.floor(darknodeIndices.length / 2)
- )) {
- await dnp.claim(ID(index));
- }
-
- // Withdraw legacy rewards for first 25 nodes.
- for (const index of darknodeIndices.slice(
- 0,
- Math.floor(darknodeIndices.length / 2)
- )) {
- if (rng() < 0.5) {
- await dnp.withdraw(ID(index), newToken.address);
- }
- }
-
- rewards = await ClaimlessRewards.new(
- dnr.address,
- store.address,
- owner,
- 50000
- );
- await dnp.transferStoreOwnership(rewards.address);
- await dnr.updateDarknodePayment(rewards.address);
- await STEPS.registerToken(rewards, [ETHEREUM, newToken.address]);
-
- // Register 50 nodes after switching to ClaimlessRewards.
- for (const index of darknodeIndices.slice(
- Math.floor(darknodeIndices.length / 2)
- )) {
- await registerNode(index);
-
- if (rng() < 0.5) {
- await STEPS.waitForEpoch(rewards);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- // Add random amount of DAI to rewards.
- await STEPS.addRewards(
- rewards,
- newToken.address,
- new BigNumber(rng())
- .times(1000)
- .times(
- new BigNumber(10).exponentiatedBy(
- await getDecimals(newToken.address)
- )
- )
- );
- }
- }
-
- // await STEPS.waitForEpoch(rewards);
-
- // // Deregister random nodes.
- // for (const index of darknodeIndices.slice(
- // 0,
- // darknodeIndices.length
- // )) {
- // if (rng() < 0.2) {
- // await deregisterNode(index);
- // }
-
- // if (rng() < 0.5) {
- // await STEPS.waitForEpoch(rewards);
-
- // // Add random amount of DAI to rewards.
- // await STEPS.addRewards(
- // dnp,
- // newToken.address,
- // new BigNumber(rng())
- // .times(1000)
- // .times(
- // new BigNumber(10).exponentiatedBy(
- // await getDecimals(newToken.address)
- // )
- // )
- // );
-
- // await STEPS.changeCycle(rewards, 1 * HOURS);
- // }
- // }
-
- await STEPS.waitForEpoch(rewards);
-
- for (const index of darknodeIndices) {
- await STEPS.withdraw(
- rewards,
- ID(index),
- newToken.address,
- accounts[index % accounts.length]
- );
- }
-
- (
- await store.darknodeBalances(NULL, newToken.address)
- ).should.bignumber.equal(0);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, newToken.address);
- await deregisterNode(darknodeIndices);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode(darknodeIndices);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("balance if pending registration is 0", async () => {
- // register ETH token and two darknodes
- await registerNode(1);
- await STEPS.registerToken(rewards, ETHEREUM);
-
- await rewards
- .darknodeBalances(ID(1), ETHEREUM)
- .should.be.rejectedWith(
- /ClaimlessRewards: registration pending/
- );
- });
- });
-
- describe("getNextEpochFromTimestamp", () => {
- it("should return the correct timestamp", async () => {
- const timestamps = await rewards.getEpochTimestamps();
-
- for (let i = 0; i < timestamps.length; i++) {
- const timestamp = new BigNumber(timestamps[i].toString());
- const nextTimestamp = timestamps[i + 1]
- ? new BigNumber(timestamps[i + 1].toString())
- : undefined;
- const previousTimestamp = timestamps[i - 1]
- ? new BigNumber(timestamps[i - 1].toString())
- : undefined;
-
- // Check that timestamps are ordered.
- if (nextTimestamp) {
- nextTimestamp.should.be.bignumber.greaterThan(timestamp);
- }
- if (previousTimestamp) {
- previousTimestamp.should.be.bignumber.lessThan(timestamp);
- }
-
- // Check that getNextEpochFromTimestamp(timestamp - 1) == timestamp
- (
- await rewards.getNextEpochFromTimestamp(
- timestamp.minus(1).toFixed()
- )
- ).should.bignumber.equal(
- previousTimestamp &&
- timestamp.minus(1).isEqualTo(previousTimestamp)
- ? previousTimestamp
- : timestamp
- );
-
- // Check that getNextEpochFromTimestamp(timestamp) == timestamp
- (
- await rewards.getNextEpochFromTimestamp(timestamp.toFixed())
- ).should.bignumber.equal(timestamp);
-
- // Check that getNextEpochFromTimestamp(timestamp + 1) == next timestamp
- (
- await rewards.getNextEpochFromTimestamp(
- timestamp.plus(1).toFixed()
- )
- ).should.bignumber.equal(nextTimestamp || new BigNumber(0));
- }
-
- if (timestamps.length) {
- (
- await rewards.getNextEpochFromTimestamp(0)
- ).should.bignumber.equal(timestamps[0]);
- }
- });
- });
-
- describe("Transferring ownership", () => {
- it("should disallow unauthorized transferring of ownership", async () => {
- await rewards
- .transferStoreOwnership(accounts[1], { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- await rewards
- .claimStoreOwnership({ from: accounts[1] })
- .should.be.rejectedWith(
- /Claimable: caller is not the pending owner/
- );
- });
-
- it("can transfer ownership of the darknode payment store", async () => {
- const newDarknodePayment = await ClaimlessRewards.new(
- dnr.address,
- store.address,
- owner,
- 50000
- );
-
- // [ACTION] Initiate ownership transfer to wrong account
- await rewards.transferStoreOwnership(newDarknodePayment.address, {
- from: accounts[0],
- });
-
- // [CHECK] Owner should be the new rewards contract.
- (await store.owner()).should.equal(newDarknodePayment.address);
-
- // [RESET] Initiate ownership transfer back to rewards.
- await newDarknodePayment.transferStoreOwnership(rewards.address);
-
- // [CHECK] Owner should now be the rewards.
- (await store.owner()).should.equal(rewards.address);
- });
- });
-
- describe("when forwarding funds", async () => {
- it("can forward ETH", async () => {
- await rewards.forward(NULL);
- });
-
- it("can forward funds to the store", async () => {
- // rewards should have zero balance
- new BN(await dai.balanceOf(rewards.address)).should.bignumber.equal(
- new BN(0)
- );
-
- const storeDaiBalance = new BN(
- await store.availableBalance(dai.address)
- );
- const amount = new BN("1000000");
- new BN(await dai.balanceOf(owner)).gte(amount).should.be.true;
- await dai.transfer(rewards.address, amount);
-
- (await store.availableBalance(dai.address)).should.bignumber.equal(
- storeDaiBalance
- );
- // rewards should have some balance
- new BN(await dai.balanceOf(rewards.address)).should.bignumber.equal(
- amount
- );
-
- // Forward the funds on
- await rewards.forward(dai.address);
- new BN(await dai.balanceOf(rewards.address)).should.bignumber.equal(
- new BN(0)
- );
- (await store.availableBalance(dai.address)).should.bignumber.equal(
- storeDaiBalance.add(amount)
- );
- });
- });
-
- describe("when changing payout proportion", async () => {
- it("cannot change payout proportion to an invalid percent", async () => {
- const denominator = await toBN(
- rewards.HOURLY_PAYOUT_WITHHELD_DENOMINATOR()
- );
- await rewards
- .updateHourlyPayoutWithheld(denominator.plus(1).toFixed())
- .should.be.rejectedWith(/ClaimlessRewards: invalid numerator/);
- });
-
- it("can change payout proportion", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Ensure there are no fees from other tests.
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18)))
- );
-
- const oldNumerator = await toBN(
- rewards.hourlyPayoutWithheldNumerator()
- );
- const denominator = await toBN(
- rewards.HOURLY_PAYOUT_WITHHELD_DENOMINATOR()
- );
- await rewards.updateHourlyPayoutWithheld(denominator.toFixed());
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- (
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1)
- ).should.bignumber.equal(0);
-
- await rewards.updateHourlyPayoutWithheld(0);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- // No rewards should have been withheld, except rounded amounts too
- // small to be distributed to all darknodes.
- const numberOfDarknodes = await toBN(dnr.numDarknodes());
- (
- await store.availableBalance(dai.address)
- ).should.bignumber.lessThan(numberOfDarknodes);
-
- await STEPS.withdraw(rewards, ID(1), dai.address, operator1);
-
- await rewards.updateHourlyPayoutWithheld(oldNumerator.toFixed());
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
- });
-
- describe("admin methods", async () => {
- it("only admin can update payout proportions", async () => {
- await rewards
- .updateHourlyPayoutWithheld(new BN(10), { from: accounts[2] })
- .should.be.rejectedWith(/Ownable: caller is not the owner./);
- });
-
- describe("DarknodeRegistry", () => {
- it("only admin can update darknode registry", async () => {
- await rewards
- .updateDarknodeRegistry(accounts[2], { from: accounts[2] })
- .should.be.rejectedWith(
- /Ownable: caller is not the owner./
- );
- });
-
- it("can update DarknodeRegistry", async () => {
- const darknodeRegistry = await rewards.darknodeRegistry();
- await rewards
- .updateDarknodeRegistry(NULL)
- .should.be.rejectedWith(
- /ClaimlessRewards: invalid Darknode Registry address/
- );
-
- await rewards.updateDarknodeRegistry(accounts[0]);
- await rewards.updateDarknodeRegistry(darknodeRegistry);
- });
- });
-
- describe("community fund", () => {
- it("only admin can update community fund", async () => {
- await rewards
- .updateCommunityFund(accounts[2], { from: accounts[2] })
- .should.be.rejectedWith(
- /Ownable: caller is not the owner./
- );
- });
-
- it("only admin can update community fund percent", async () => {
- await rewards
- .updateCommunityFundNumerator(0, { from: accounts[2] })
- .should.be.rejectedWith(
- /Ownable: caller is not the owner./
- );
- });
-
- it("can withdraw community fund rewards", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- (
- await STEPS.withdrawToCommunityFund(rewards, [
- ETHEREUM,
- dai.address,
- ])
- ).should.bignumber.greaterThan(0);
-
- // Second time - empty values.
- (
- await STEPS.withdrawToCommunityFund(rewards, [
- ETHEREUM,
- dai.address,
- ])
- ).should.bignumber.equal(0);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- });
-
- it("can update community fund", async () => {
- const communityFund = await rewards.communityFund();
- await rewards.updateCommunityFund(accounts[0]);
- await rewards.updateCommunityFund(communityFund);
- });
-
- it("cannot change community fund percent to an invalid percent", async () => {
- const denominator = await toBN(
- rewards.HOURLY_PAYOUT_WITHHELD_DENOMINATOR()
- );
- await rewards
- .updateCommunityFundNumerator(denominator.plus(1).toFixed())
- .should.be.rejectedWith(
- /ClaimlessRewards: invalid numerator/
- );
- });
-
- it("can update community fund percent", async () => {
- // register ETH token and two darknodes
- await registerNode([1, 2]);
- await STEPS.registerToken(rewards, [ETHEREUM, dai.address]);
- await STEPS.waitForEpoch(rewards);
-
- // Ensure there are no community funds from previous tests.
- (
- await STEPS.withdrawToCommunityFund(rewards, [
- ETHEREUM,
- dai.address,
- ])
- ).should.bignumber.greaterThan(0);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18))).add(new BN(1))
- );
-
- // Add 1 ETH to rewards.
- await STEPS.addRewards(
- rewards,
- ETHEREUM,
- new BN(1).mul(new BN(10).pow(new BN(18)))
- );
-
- // Update community fund numerator to 0.
- const oldCommunityFundPercent = await toBN(
- rewards.communityFundNumerator()
- );
- await rewards.updateCommunityFundNumerator(0);
-
- await STEPS.changeCycle(rewards, 28 * DAYS);
-
- (
- await STEPS.withdrawToCommunityFund(rewards, [
- ETHEREUM,
- dai.address,
- ])
- ).should.bignumber.equal(0);
-
- // Second time - empty values.
- (
- await STEPS.withdrawToCommunityFund(rewards, [
- ETHEREUM,
- dai.address,
- ])
- ).should.bignumber.equal(0);
-
- await STEPS.deregisterToken(rewards, ETHEREUM);
- await STEPS.deregisterToken(rewards, dai.address);
- await deregisterNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
- await STEPS.waitForEpoch(rewards);
- await refundNode([1, 2]);
- await STEPS.waitForEpoch(rewards);
-
- // Revert community fund numerator change.
- await rewards.updateCommunityFundNumerator(
- oldCommunityFundPercent.toFixed()
- );
- });
-
- it("can't set community fund to 0x0 or registered darknode", async () => {
- await rewards
- .updateCommunityFund(NULL)
- .should.be.rejectedWith(
- /ClaimlessRewards: invalid community fund address/
- );
-
- await registerNode(1);
- await rewards
- .updateCommunityFund(ID(1))
- .should.be.rejectedWith(
- /ClaimlessRewards: community fund must not be a registered darknode/
- );
- });
-
- it("malicious operator can't withdraw community fund", async () => {
- const communityFund = await rewards.communityFund();
- const malicious = accounts[4];
- await ren.transfer(malicious, MINIMUM_BOND);
- await ren.approve(dnr.address, MINIMUM_BOND, {
- from: malicious,
- });
- // Register the darknodes under the account address
- await dnr.register(communityFund, PUBK(-1), {
- from: malicious,
- });
- await STEPS.waitForEpoch(rewards);
-
- // Add 101.00...01 DAI to rewards.
- await STEPS.addRewards(
- rewards,
- dai.address,
- new BN(101).mul(new BN(10).pow(new BN(18)))
- );
- await STEPS.changeCycle(rewards, 1 * HOURS);
-
- await STEPS.withdraw(
- rewards,
- communityFund,
- dai.address
- ).should.be.rejectedWith(/ClaimlessRewards: invalid node ID/);
- });
- });
- });
-
- const registerNode = async (array: number | number[], from?: string) => {
- array = Array.isArray(array) ? array : [array];
- for (const i of array) {
- await ren.transfer(
- from || accounts[i % accounts.length],
- MINIMUM_BOND
- );
- await ren.approve(dnr.address, MINIMUM_BOND, {
- from: from || accounts[i % accounts.length],
- });
- // Register the darknodes under the account address
- await dnr.register(ID(i), PUBK(i), {
- from: from || accounts[i % accounts.length],
- });
- }
- };
-
- const deregisterNode = async (array: number | number[], from?: string) => {
- array = Array.isArray(array) ? array : [array];
- for (const i of array) {
- await dnr.deregister(ID(i), {
- from: from || accounts[i % accounts.length],
- });
- }
- };
-
- const refundNode = async (array: number | number[], from?: string) => {
- array = Array.isArray(array) ? array : [array];
- for (const i of array) {
- await dnr.refund(ID(i), {
- from: from || accounts[i % accounts.length],
- });
- }
- };
-});
diff --git a/test/ClaimlessRewards/steps.ts b/test/ClaimlessRewards/steps.ts
deleted file mode 100644
index 650d779e..00000000
--- a/test/ClaimlessRewards/steps.ts
+++ /dev/null
@@ -1,487 +0,0 @@
-import BigNumber from "bignumber.js";
-import { OrderedMap } from "immutable";
-import moment from "moment";
-import {
- ClaimlessRewardsInstance,
- DarknodePaymentInstance,
- DarknodePaymentStoreContract,
- DarknodeRegistryLogicV1Contract
-} from "../../types/truffle-contracts";
-import {
- ETHEREUM,
- getBalance,
- getDecimals,
- getSymbol,
- HOURS,
- increaseTime,
- NULL,
- toBN,
- transferToken,
- waitForEpoch
-} from "../helper/testUtils";
-
-const DarknodePaymentStore: DarknodePaymentStoreContract = artifacts.require(
- "DarknodePaymentStore"
-);
-const DarknodeRegistry: DarknodeRegistryLogicV1Contract = artifacts.require(
- "DarknodeRegistryLogicV1"
-);
-
-const registerToken = async (
- rewards: ClaimlessRewardsInstance,
- tokens: string | string[]
-) => {
- tokens = Array.isArray(tokens) ? tokens : [tokens];
-
- for (const token of tokens) {
- // Precondition. The token is not registered.
- (await rewards.isRegistered(token)).should.equal(false);
- const allTokens = await rewards.getRegisteredTokens();
-
- // Effect. Register the token.
- await rewards.registerToken(token);
-
- // Postcondition. The token is registered.
- (await rewards.isRegistered(token)).should.equal(true);
- (await rewards.getRegisteredTokens()).should.deep.equal([
- ...allTokens,
- token
- ]);
- }
-};
-
-const deregisterToken = async (
- rewards: ClaimlessRewardsInstance,
- tokens: string | string[]
-) => {
- tokens = Array.isArray(tokens) ? tokens : [tokens];
-
- for (const token of tokens) {
- // Precondition. The token is registered.
- (await rewards.isRegistered(token)).should.equal(true);
- const allTokens = await rewards.getRegisteredTokens();
-
- // Effect. Deregister the token.
- await rewards.deregisterToken(token);
-
- // Postcondition. The token is not registered.
- (await rewards.isRegistered(token)).should.equal(false);
- (await rewards.getRegisteredTokens()).should.deep.equal(
- allTokens.filter(x => x !== token)
- );
- }
-};
-
-const changeCycle = async (
- rewards: ClaimlessRewardsInstance,
- time: number,
- epoch?: boolean
-) => {
- const latestTimestamp = await toBN(rewards.latestCycleTimestamp());
- const storeAddress = await rewards.store();
- const store = await DarknodePaymentStore.at(storeAddress);
- const dnrAddress = await rewards.darknodeRegistry();
- const dnr = await DarknodeRegistry.at(dnrAddress);
- const communityFund = await rewards.communityFund();
-
- const tokens = await rewards.getRegisteredTokens();
- let freeBeforeMap = OrderedMap();
- let communityFundBalanceBeforeMap = OrderedMap();
- let darknodePoolBeforeMap = OrderedMap();
- let shareBeforeMap = OrderedMap();
- for (const token of tokens) {
- const freeBefore = await toBN(store.availableBalance(token));
- freeBeforeMap = freeBeforeMap.set(token, freeBefore);
-
- const communityFundBalanceBefore = await toBN(
- rewards.darknodeBalances(communityFund, token)
- );
- communityFundBalanceBeforeMap = communityFundBalanceBeforeMap.set(
- token,
- communityFundBalanceBefore
- );
- const darknodePoolBefore = await toBN(
- rewards.darknodeBalances(NULL, token)
- );
- darknodePoolBeforeMap = darknodePoolBeforeMap.set(
- token,
- darknodePoolBefore
- );
- const shareBefore = await toBN(
- rewards.cycleCumulativeTokenShares(latestTimestamp.toFixed(), token)
- );
- shareBeforeMap = shareBeforeMap.set(token, shareBefore);
- }
- const shares = await toBN(dnr.numDarknodes());
- const epochTimestampCountBefore = await toBN(
- rewards.epochTimestampsLength()
- );
-
- // Effect. Change the cycle.
- let tx;
- if (epoch) {
- tx = await waitForEpoch(dnr);
- } else {
- await increaseTime(time);
- tx = await rewards.changeCycle();
- }
-
- // Postcondition. Check that the cycle's timestamp is stored correctly.
- const block = await web3.eth.getBlock(tx.receipt.blockNumber);
- const timestamp = new BigNumber(block.timestamp);
- const newLatestTimestamp = await toBN(rewards.latestCycleTimestamp());
- // Check if the epoch happened too recently to a cycle, so no cycle was
- // called.
- const expectedTimestamp = epoch
- ? timestamp
- : latestTimestamp.plus(
- timestamp
- .minus(latestTimestamp)
- .minus(timestamp.minus(latestTimestamp).mod(1 * HOURS))
- );
- newLatestTimestamp.should.not.bignumber.equal(latestTimestamp);
- newLatestTimestamp.should.bignumber.equal(expectedTimestamp);
- const epochTimestampCountAfter = await toBN(
- rewards.epochTimestampsLength()
- );
- if (epoch) {
- epochTimestampCountAfter.should.bignumber.equal(
- epochTimestampCountBefore.plus(1)
- );
- }
-
- // Postcondition. Check conditions for each token.
- const hours = timestamp
- .minus(latestTimestamp)
- .dividedToIntegerBy(1 * HOURS)
- .toNumber();
- const numerator = await toBN(rewards.hourlyPayoutWithheldNumerator());
- const denominator = await toBN(
- rewards.HOURLY_PAYOUT_WITHHELD_DENOMINATOR()
- );
- let numeratorSeries = numerator;
- for (let i = 0; i < hours; i++) {
- numeratorSeries = numeratorSeries
- .times(numerator)
- .div(denominator)
- .integerValue(BigNumber.ROUND_DOWN);
- }
- const communityFundNumerator = await toBN(rewards.communityFundNumerator());
-
- for (const token of tokens) {
- const freeBefore = freeBeforeMap.get(token);
- const communityFundBalanceBefore = communityFundBalanceBeforeMap.get(
- token
- );
- const darknodePoolBefore = darknodePoolBeforeMap.get(token);
- const shareBefore = shareBeforeMap.get(token);
-
- const totalWithheld = freeBefore
- .times(numeratorSeries)
- .div(denominator)
- .integerValue(BigNumber.ROUND_DOWN);
-
- const totalPaidout = freeBefore.minus(totalWithheld);
- const communityFundPaidout = totalPaidout
- .times(communityFundNumerator)
- .div(denominator)
- .integerValue(BigNumber.ROUND_DOWN);
-
- const darknodePaidout = totalPaidout.minus(communityFundPaidout);
- const share = shares.isZero()
- ? new BigNumber(0)
- : darknodePaidout.div(shares).integerValue(BigNumber.ROUND_DOWN);
-
- const darknodePaidoutAdjusted = share.times(shares);
-
- // Postcondition. The stored share is the correct amount.
- const shareAfter = await toBN(
- rewards.cycleCumulativeTokenShares(
- newLatestTimestamp.toFixed(),
- token
- )
- );
-
- shareAfter.minus(shareBefore).should.bignumber.equal(share);
-
- // Postcondition. The darknode pool increased by the correct amount.
- const darknodePoolAfter = await toBN(
- rewards.darknodeBalances(NULL, token)
- );
- darknodePoolAfter
- .minus(darknodePoolBefore)
- .should.bignumber.equal(darknodePaidoutAdjusted);
-
- // Postcondition. The community fund increased by the correct amount.
- const communityFundBalanceAfter = await toBN(
- rewards.darknodeBalances(communityFund, token)
- );
- communityFundBalanceAfter
- .minus(communityFundBalanceBefore)
- .should.bignumber.equal(communityFundPaidout);
-
- // Postcondition. The free amount decreased by the correct amount.
- const freeAfter = await toBN(store.availableBalance(token));
- freeBefore
- .minus(freeAfter)
- .should.bignumber.equal(
- communityFundPaidout.plus(darknodePaidoutAdjusted)
- );
- }
-
- console.log(
- `New cycle after ${moment
- .duration(newLatestTimestamp.minus(latestTimestamp).times(1000))
- .humanize()}.`
- );
-
- return hours;
-};
-
-const _waitForEpoch = async (rewards: ClaimlessRewardsInstance) => {
- await changeCycle(rewards, 0, true);
-};
-
-const addRewards = async (
- rewards: ClaimlessRewardsInstance | DarknodePaymentInstance,
- token: string,
- amount: BigNumber | number | string | BN
-) => {
- const storeAddress = await rewards.store();
- const balanceBefore = await getBalance(token, storeAddress);
- const store = await DarknodePaymentStore.at(storeAddress);
- const freeBefore = await toBN(store.availableBalance(token));
-
- // Effect. Transfer token to the store contract.
- await transferToken(token, storeAddress, amount);
-
- // Postcondition. The balance after has increased by the amount added.
- const balanceAfter = await getBalance(token, storeAddress);
- balanceAfter.minus(balanceBefore).should.bignumber.equal(amount);
- const freeAfter = await toBN(store.availableBalance(token));
- freeAfter.minus(freeBefore).should.bignumber.equal(amount);
-
- console.log(
- `There are now ${new BigNumber(freeAfter.toString())
- .div(new BigNumber(10).exponentiatedBy(await getDecimals(token)))
- .toFixed()} ${await getSymbol(token)} in rewards`
- );
-};
-
-const withdraw = async (
- rewards: ClaimlessRewardsInstance,
- darknodes: string | string[],
- tokens: string | string[],
- from?: string
-) => {
- tokens = Array.isArray(tokens) ? tokens : [tokens];
- darknodes = Array.isArray(darknodes) ? darknodes : [darknodes];
- from = from || darknodes[0];
-
- // Store the balance for each token, and the withdrawable amount for each
- // darknode and token.
- let withdrawableMap = OrderedMap>();
- let balanceBeforeMap = OrderedMap();
- // let legacyBalanceMap = OrderedMap>();
- // let shareBeforeMap = OrderedMap>();
- const storeAddress = await rewards.store();
- const store = await DarknodePaymentStore.at(storeAddress);
- const currentCycle = await toBN(rewards.latestCycleTimestamp());
- const dnrAddress = await rewards.darknodeRegistry();
- const dnr = await DarknodeRegistry.at(dnrAddress);
- for (const token of tokens) {
- const balanceBefore = await getBalance(token, from);
- balanceBeforeMap = balanceBeforeMap.set(token, balanceBefore);
-
- for (const darknode of darknodes) {
- const withdrawable = await toBN(
- rewards.darknodeBalances(darknode, token)
- );
- withdrawableMap = withdrawableMap.set(
- darknode,
- (
- withdrawableMap.get(darknode) ||
- OrderedMap()
- ).set(token, withdrawable)
- );
-
- // Precondition. The withdrawable amount should be the correct
- // amount, including any legacy balance left-over.
- const nodeRegistered = await toBN(
- dnr.darknodeRegisteredAt(darknode)
- );
- const nodeDeregistered = await toBN(
- dnr.darknodeDeregisteredAt(darknode)
- );
- // Node not registered.
- if (nodeRegistered.isZero()) {
- continue;
- }
- const legacyBalance = await toBN(
- store.darknodeBalances(darknode, token)
- );
- let lastWithdrawn = await toBN(
- rewards.rewardsLastClaimed(darknode, token)
- );
- if (lastWithdrawn.lt(nodeRegistered)) {
- lastWithdrawn = await toBN(
- rewards.getNextEpochFromTimestamp(nodeRegistered.toFixed())
- );
- }
- let claimableUntil = currentCycle;
- if (nodeDeregistered.isGreaterThan(0)) {
- const deregisteredCycle = await toBN(
- rewards.getNextEpochFromTimestamp(
- nodeDeregistered.toFixed()
- )
- );
- if (deregisteredCycle.isGreaterThan(0)) {
- claimableUntil = deregisteredCycle;
- }
- }
- const shareBefore = await toBN(
- rewards.cycleCumulativeTokenShares(
- lastWithdrawn.toFixed(),
- token
- )
- );
- const shareAfter = await toBN(
- rewards.cycleCumulativeTokenShares(
- claimableUntil.toFixed(),
- token
- )
- );
- withdrawable
- .minus(legacyBalance)
- .should.bignumber.equal(shareAfter.minus(shareBefore));
- }
- }
-
- // Effect.
- let tx;
- if (tokens.length !== 1) {
- tx = await rewards.withdrawMultiple(darknodes, tokens, { from });
- } else if (darknodes.length !== 1) {
- tx = await rewards.withdrawToken(darknodes, tokens[0], { from });
- } else {
- tx = await rewards.withdraw(darknodes[0], tokens[0], { from });
- }
-
- // Postcondition. Check conditions for each token and darknode.
- for (const token of tokens) {
- const balanceBefore = balanceBeforeMap.get(token);
-
- let withdrawableSum = new BigNumber(0);
- for (const darknode of darknodes) {
- const withdrawable = withdrawableMap.get(darknode).get(token);
- withdrawableSum = withdrawableSum.plus(withdrawable);
-
- const postWithdrawable = await toBN(
- rewards.darknodeBalances(darknode, token)
- );
- postWithdrawable.should.bignumber.equal(0);
-
- console.log(
- `${darknode.slice(0, 8)}... withdrew ${withdrawable
- .div(
- new BigNumber(10).exponentiatedBy(
- await getDecimals(token)
- )
- )
- .toFixed()} ${await getSymbol(token)}`
- );
- }
-
- // Postcondition. The token balance of the user withdrawing increased
- // by the expected amount.
- const transactionDetails = await web3.eth.getTransaction(tx.tx);
- let gasFee = new BigNumber(0);
- if (token === ETHEREUM) {
- const { gasPrice } = transactionDetails;
- const { gasUsed } = tx.receipt;
- gasFee = new BigNumber(gasUsed).times(gasPrice);
- }
- (await getBalance(token, from)).should.bignumber.equal(
- balanceBefore.plus(withdrawableSum).minus(gasFee)
- );
- }
-
- if (darknodes.length && tokens.length) {
- return withdrawableMap.get(darknodes[0]).get(tokens[0]);
- } else {
- return new BigNumber(0);
- }
-};
-
-const withdrawToCommunityFund = async (
- rewards: ClaimlessRewardsInstance,
- tokens: string | string[],
- from?: string
-) => {
- from = from || (await web3.eth.getAccounts())[0];
- tokens = Array.isArray(tokens) ? tokens : [tokens];
-
- // Store the balance for each token, and the withdrawable amount for each
- // darknode and token.
- const communityFund = await rewards.communityFund();
- let withdrawableMap = OrderedMap();
- let balanceBeforeMap = OrderedMap();
- for (const token of tokens) {
- const balanceBefore = await getBalance(token, communityFund);
- balanceBeforeMap = balanceBeforeMap.set(token, balanceBefore);
-
- const withdrawable = await toBN(
- rewards.darknodeBalances(communityFund, token)
- );
- withdrawableMap = withdrawableMap.set(token, withdrawable);
- }
-
- // Effect.
- const tx = await rewards.withdrawToCommunityFund(tokens);
-
- // Postcondition. Check conditions for each token and darknode.
- for (const token of tokens) {
- const balanceBefore = balanceBeforeMap.get(token);
- const withdrawableBefore = withdrawableMap.get(token);
-
- console.log(
- `Withdrew ${withdrawableBefore
- .div(
- new BigNumber(10).exponentiatedBy(await getDecimals(token))
- )
- .toFixed()} ${await getSymbol(token)} to the community fund.`
- );
-
- // Postcondition. The token balance of the user withdrawing increased
- // by the expected amount.
- const transactionDetails = await web3.eth.getTransaction(tx.tx);
- let gasFee = new BigNumber(0);
- if (token === ETHEREUM && from === communityFund) {
- const { gasPrice } = transactionDetails;
- const { gasUsed } = tx.receipt;
- gasFee = new BigNumber(gasUsed).times(gasPrice);
- }
- (await getBalance(token, communityFund)).should.bignumber.equal(
- balanceBefore.plus(withdrawableBefore).minus(gasFee)
- );
- (
- await toBN(rewards.darknodeBalances(communityFund, token))
- ).should.bignumber.equal(0);
- }
-
- if (tokens.length) {
- return withdrawableMap.get(tokens[0]);
- } else {
- return new BigNumber(0);
- }
-};
-
-export const STEPS = {
- registerToken,
- deregisterToken,
- changeCycle,
- addRewards,
- withdraw,
- withdrawToCommunityFund,
- waitForEpoch: _waitForEpoch
-};
diff --git a/test/Compare.ts b/test/Compare.ts
deleted file mode 100644
index 057a86b9..00000000
--- a/test/Compare.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { expect } from "chai";
-
-import { CompareTestInstance } from "../types/truffle-contracts";
-
-const CompareTest = artifacts.require("CompareTest");
-
-contract("Compare", accounts => {
- let CompareInstance: CompareTestInstance;
-
- before(async () => {
- CompareInstance = await CompareTest.new();
- });
-
- describe("when bytes are the same length", async () => {
- it("should return false when content is different", async () => {
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const blockhash2 = "sdkjfhaefhefjhskjefjhefhjksefkehjfsjc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- expect(
- await CompareInstance.bytesEqual(hexBlockhash1, hexBlockhash2)
- ).to.be.false;
- });
-
- it("should return true when content is the same", async () => {
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- expect(
- await CompareInstance.bytesEqual(hexBlockhash1, hexBlockhash1)
- ).to.be.true;
- const hexBlockhash2 = web3.utils.asciiToHex("abcdefghijk");
- expect(
- await CompareInstance.bytesEqual(hexBlockhash2, hexBlockhash2)
- ).to.be.true;
- const hexBlockhash3 = web3.utils.asciiToHex(
- "hukrasefaakuflehlafsefhuha2h293f8"
- );
- expect(
- await CompareInstance.bytesEqual(hexBlockhash3, hexBlockhash3)
- ).to.be.true;
- });
- });
-
- describe("when bytes are of different length", async () => {
- it("should return false", async () => {
- const blockhash1 = "XTsJ2rO2yD47tg3J";
- const blockhash2 = "sdkjfhaefhefjhskjefjhefhjksefkehjfsjc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- expect(
- await CompareInstance.bytesEqual(hexBlockhash1, hexBlockhash2)
- ).to.be.false;
- });
- });
-});
diff --git a/test/DarknodePayment.ts b/test/DarknodePayment.ts
deleted file mode 100644
index 44ae8724..00000000
--- a/test/DarknodePayment.ts
+++ /dev/null
@@ -1,1109 +0,0 @@
-import BN from "bn.js";
-
-import {
- DarknodePaymentInstance,
- DarknodePaymentStoreInstance,
- DarknodeRegistryForwarderInstance,
- DarknodeRegistryLogicV1Instance,
- DarknodeSlasherInstance,
- ERC20Instance,
- RenTokenInstance,
- DarknodePaymentMigratorInstance
-} from "../types/truffle-contracts";
-import {
- ETHEREUM,
- MINIMUM_BOND,
- NULL,
- PUBK,
- waitForEpoch
-} from "./helper/testUtils";
-
-const Claimer = artifacts.require("Claimer");
-const RenToken = artifacts.require("RenToken");
-const ERC20 = artifacts.require("PaymentToken");
-const DarknodePaymentStore = artifacts.require("DarknodePaymentStore");
-const DarknodePayment = artifacts.require("DarknodePayment");
-const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const SelfDestructingToken = artifacts.require("SelfDestructingToken");
-const DarknodeSlasher = artifacts.require("DarknodeSlasher");
-// const DarknodeRegistryForwarder = artifacts.require(
-// "DarknodeRegistryForwarder"
-// );
-const DarknodePaymentMigrator = artifacts.require("DarknodePaymentMigrator");
-
-const { config } = require("../migrations/networks");
-
-contract("DarknodePayment", (accounts: string[]) => {
- let store: DarknodePaymentStoreInstance;
- let dai: ERC20Instance;
- let erc20Token: ERC20Instance;
- let dnr: DarknodeRegistryLogicV1Instance;
- let dnp: DarknodePaymentInstance;
- let ren: RenTokenInstance;
- let slasher: DarknodeSlasherInstance;
- // let forwarder: DarknodeRegistryForwarderInstance;
-
- const owner = accounts[0];
- const darknode1 = accounts[1];
- const darknode2 = accounts[2];
- const darknode3 = accounts[3];
- const darknode6 = accounts[6];
-
- before(async () => {
- ren = await RenToken.deployed();
- dai = await ERC20.new("DAI");
- erc20Token = await ERC20.new("ERC20");
- const dnrProxy = await DarknodeRegistryProxy.deployed();
- dnr = await DarknodeRegistryLogicV1.at(dnrProxy.address);
- store = await DarknodePaymentStore.deployed();
- dnp = await DarknodePayment.deployed();
- slasher = await DarknodeSlasher.deployed();
- await dnr.updateSlasher(slasher.address);
-
- // forwarder = await DarknodeRegistryForwarder.new(dnr.address);
- // await dnp.updateDarknodeRegistry(forwarder.address);
-
- await waitForEpoch(dnr);
-
- new BN(await dnr.numDarknodes()).should.bignumber.equal(new BN(0));
- });
-
- afterEach(async () => {
- await waitForEpoch(dnr);
- });
-
- describe("Token registration", async () => {
- const tokenCount = async () => {
- let i = 0;
- while (true) {
- try {
- await dnp.registeredTokens(i);
- i++;
- } catch (error) {
- break;
- }
- }
- return i;
- };
-
- const printTokens = async () => {
- console.info(`Registered tokens: [`);
- let i = 0;
- while (true) {
- try {
- const token = await dnp.registeredTokens(i);
- console.info(
- ` ${token}, (${await dnp.registeredTokenIndex(
- token
- )})`
- );
- i++;
- } catch (error) {
- break;
- }
- }
- console.info(`]`);
- };
-
- const checkTokenIndexes = async () => {
- let i = 0;
- while (true) {
- try {
- const token = await dnp.registeredTokens(i);
- (
- await dnp.registeredTokenIndex(token)
- ).should.bignumber.equal(i + 1);
- i++;
- } catch (error) {
- if (error.toString().match("invalid opcode")) {
- break;
- }
- await printTokens();
- throw error;
- }
- }
- };
-
- it("cannot register token if not owner", async () => {
- await dnp
- .registerToken(dai.address, { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- });
-
- it("can register tokens", async () => {
- const lengthBefore = await tokenCount();
-
- (await dnp.tokenPendingRegistration(dai.address)).should.equal(
- false
- );
- (
- await dnp.tokenPendingRegistration(erc20Token.address)
- ).should.equal(false);
-
- await dnp.registerToken(dai.address);
- (await dnp.tokenPendingRegistration(dai.address)).should.equal(
- true
- );
- await dnp
- .registerToken(dai.address)
- .should.be.rejectedWith(
- /DarknodePayment: token already pending registration/
- );
- await dnp.registerToken(erc20Token.address);
- (
- await dnp.tokenPendingRegistration(erc20Token.address)
- ).should.equal(true);
- // complete token registration
- await waitForEpoch(dnr);
- (await dnp.registeredTokens(lengthBefore)).should.equal(
- dai.address
- );
- (
- await dnp.registeredTokenIndex(dai.address)
- ).should.bignumber.equal(new BN(lengthBefore + 1));
- (await dnp.tokenPendingRegistration(dai.address)).should.equal(
- false
- );
- (
- await dnp.tokenPendingRegistration(erc20Token.address)
- ).should.equal(false);
-
- await dnp.registerToken(ETHEREUM);
- // complete token registration
- await waitForEpoch(dnr);
- (await dnp.registeredTokens(lengthBefore + 2)).should.equal(
- ETHEREUM
- );
- (await dnp.registeredTokenIndex(ETHEREUM)).should.bignumber.equal(
- lengthBefore + 3
- );
- await checkTokenIndexes();
-
- (await dnp.tokenPendingRegistration(dai.address)).should.equal(
- false
- );
- (
- await dnp.tokenPendingRegistration(erc20Token.address)
- ).should.equal(false);
- });
-
- it("can deregister a destroyed token", async () => {
- await registerDarknode(6);
- await waitForEpoch(dnr);
- await waitForEpoch(dnr);
- // Claim so that the darknode share count isn't 0.
- await dnp.claim(darknode6);
- const sdt = await SelfDestructingToken.new();
- await dnp.registerToken(sdt.address);
- await waitForEpoch(dnr);
- await sdt.destruct();
- await dnp.deregisterToken(sdt.address);
- await waitForEpoch(dnr);
- await slasher.blacklist(darknode6);
- });
-
- it("cannot register already registered tokens", async () => {
- await dnp
- .registerToken(dai.address)
- .should.be.rejectedWith(
- /DarknodePayment: token already registered/
- );
- });
-
- it("cannot deregister token if not owner", async () => {
- await dnp
- .deregisterToken(ETHEREUM, { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- });
-
- it("can deregister tokens", async () => {
- await dnp.deregisterToken(ETHEREUM);
- await dnp
- .deregisterToken(ETHEREUM)
- .should.be.rejectedWith(
- /DarknodePayment: token not registered/
- );
- await dnp.deregisterToken(erc20Token.address);
- // check token deregistration
- (await dnp.registeredTokenIndex(ETHEREUM)).should.bignumber.equal(
- 0
- );
- (
- await dnp.registeredTokenIndex(erc20Token.address)
- ).should.bignumber.equal(0);
- await checkTokenIndexes();
- });
-
- it("cannot deregister unregistered tokens", async () => {
- await dnp
- .deregisterToken(ETHEREUM)
- .should.be.rejectedWith(
- /DarknodePayment: token not registered/
- );
- });
-
- it("properly sets index", async () => {
- const token1 = await ERC20.new("TOKEN1");
- const token2 = await ERC20.new("TOKEN2");
- const token3 = await ERC20.new("TOKEN3");
- const one = token1.address;
- const two = token2.address;
- const three = token3.address;
-
- await checkTokenIndexes();
- await dnp.registerToken(one);
- await dnp.registerToken(two);
- await dnp.registerToken(three);
- await waitForEpoch(dnr);
- await checkTokenIndexes();
-
- // const expected = await dnp.registeredTokenIndex(one);
- await dnp.deregisterToken(one);
- await waitForEpoch(dnr);
- await checkTokenIndexes();
- // (await dnp.registeredTokenIndex(two)).should.bignumber.equal(expected);
- await dnp.deregisterToken(two);
- await dnp.deregisterToken(three);
- await checkTokenIndexes();
- await waitForEpoch(dnr);
- await checkTokenIndexes();
- });
- });
-
- describe("Token deposits", async () => {
- it("can deposit ETH using deposit()", async () => {
- // deposit using deposit() function
- const previousReward = new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- );
- const oldETHBalance = new BN(await store.totalBalance(ETHEREUM));
- const amount = new BN("1000000000");
- // make sure we have enough balance
- const ownerBalance = new BN(await web3.eth.getBalance(owner));
- ownerBalance.gte(amount).should.be.true;
- await dnp.deposit(amount, ETHEREUM, {
- value: amount.toString(),
- from: accounts[0]
- });
- new BN(await store.totalBalance(ETHEREUM)).should.bignumber.equal(
- oldETHBalance.add(amount)
- );
- // We should have increased the reward pool
- new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- ).should.bignumber.equal(
- await asRewardPoolBalance(previousReward.add(amount))
- );
- });
-
- it("can deposit ETH via direct payment to DarknodePayment contract", async () => {
- // deposit using direct deposit to dnp
- const oldETHBalance = new BN(await store.totalBalance(ETHEREUM));
- const amount = new BN("1000000000");
- // make sure we have enough balance
- const ownerBalance = new BN(await web3.eth.getBalance(owner));
- ownerBalance.gte(amount).should.be.true;
- await web3.eth.sendTransaction({
- to: dnp.address,
- from: owner,
- value: amount.toString()
- });
- new BN(await store.totalBalance(ETHEREUM)).should.bignumber.equal(
- oldETHBalance.add(amount)
- );
- // We should have increased the reward pool
- new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- ).should.bignumber.equal(
- await asRewardPoolBalance(oldETHBalance.add(amount))
- );
- });
-
- it("can deposit ETH via direct payment to DarknodePaymentStore contract", async () => {
- // deposit using direct deposit to store
- const oldETHBalance = new BN(await store.totalBalance(ETHEREUM));
- const amount = new BN("1000000000");
- await web3.eth.sendTransaction({
- to: store.address,
- from: owner,
- value: amount.toString()
- });
- new BN(await store.totalBalance(ETHEREUM)).should.bignumber.equal(
- oldETHBalance.add(amount)
- );
- // We should have increased the reward pool
- new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- ).should.bignumber.equal(
- await asRewardPoolBalance(oldETHBalance.add(amount))
- );
- });
-
- it("cannot deposit ERC20 with ETH attached", async () => {
- const amount = new BN("100000000000000000");
- await dnp
- .deposit(amount, dai.address, { value: "1", from: accounts[0] })
- .should.be.rejectedWith(
- /DarknodePayment: unexpected ether transfer/
- );
- });
-
- it("cannot deposit ERC20 that has not been registered", async () => {
- const before = new BN(await dai.balanceOf(accounts[0]));
-
- // Deregister dai and try to deposit
- await dnp.deregisterToken(dai.address);
- await waitForEpoch(dnr);
-
- // Approve and deposit
- await dai.approve(dnp.address, before);
- await dnp
- .deposit(before, dai.address, { from: accounts[0] })
- .should.be.rejectedWith(
- /DarknodePayment: token not registered/
- );
-
- // RESET: Register dai back
- await dnp.registerToken(dai.address);
- await waitForEpoch(dnr);
- });
- });
-
- describe("Claiming rewards", async () => {
- it("cannot tick if not registered", async () => {
- await dnp
- .claim(accounts[0])
- .should.be.rejectedWith(
- /DarknodePayment: darknode is not registered/
- );
- });
-
- it("cannot withdraw if there is no balance", async () => {
- await registerDarknode(1);
- await waitForEpoch(dnr);
- await waitForEpoch(dnr);
-
- const balanceBefore = await dai.balanceOf(owner);
- await dnp.withdraw(darknode1, dai.address);
- const balanceAfter = await dai.balanceOf(owner);
- balanceAfter.should.be.bignumber.equal(balanceBefore);
- });
-
- it("can be paid DAI from a payee", async () => {
- // darknode1 is whitelisted and can participate in rewards
- const previousBalance = new BN(
- await store.totalBalance(dai.address)
- );
- previousBalance.should.bignumber.equal(new BN(0));
- // sanity check that the reward pool is also zero
- (await fetchRewardPool(dai.address)).should.bignumber.equal(
- new BN(0)
- );
-
- // amount we're going to top up
- const amount = new BN("100000000000000000");
- await depositDai(amount);
- // We should have increased the reward pool
- const newRewardPool = await fetchRewardPool(dai.address);
- newRewardPool.should.bignumber.equal(
- await asRewardPoolBalance(amount)
- );
-
- // We should have zero claimed balance before ticking
- new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- ).should.bignumber.equal(new BN(0));
-
- // We don't need to claim since we weren't allocated rewards last cycle
- // But claim shouldn't revert
- await dnp.claim(darknode1);
- await waitForEpoch(dnr);
-
- const lastCycleRewards = await asRewardPoolBalance(amount);
- // We should be the only one who participated last cycle
- new BN(
- await dnr.numDarknodesPreviousEpoch()
- ).should.bignumber.equal(1);
- // We should be allocated all the rewards
- new BN(
- await dnp.unclaimedRewards(dai.address)
- ).should.bignumber.equal(lastCycleRewards);
- new BN(
- await dnp.previousCycleRewardShare(dai.address)
- ).should.bignumber.equal(lastCycleRewards);
-
- // Claim the rewards for last cycle
- await dnp.claim(darknode1);
- await waitForEpoch(dnr);
-
- const pool = await fetchRewardPool(dai.address);
- const entireDAIPool = new BN(
- await dnp.unclaimedRewards(dai.address)
- );
- entireDAIPool.should.bignumber.equal(
- await asRewardPoolBalance(lastCycleRewards)
- );
- pool.should.bignumber.equal(
- await asRewardPoolBalance(entireDAIPool)
- );
- const darknode1Balance = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- darknode1Balance.should.bignumber.equal(lastCycleRewards);
-
- // store.darknodeBalances should return the same as dnp.darknodeBalances
- (
- await store.darknodeBalances(darknode1, dai.address)
- ).should.bignumber.equal(darknode1Balance);
- });
-
- it("can be paid ETH from a payee", async () => {
- // register ETH
- await dnp.registerToken(ETHEREUM);
- await waitForEpoch(dnr);
- // ETH is now a registered token, claiming should now allocate balances
-
- const oldETHBalance = new BN(await store.totalBalance(ETHEREUM));
- const amount = new BN("1000000000");
- await dnp
- .deposit(amount, ETHEREUM)
- .should.be.rejectedWith(
- /DarknodePayment: mismatched deposit value/
- );
- await dnp.deposit(amount, ETHEREUM, {
- value: amount.toString(),
- from: accounts[0]
- });
- new BN(await store.totalBalance(ETHEREUM)).should.bignumber.equal(
- oldETHBalance.add(amount)
- );
- // We should have increased the reward pool
- const newReward = new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- );
- newReward.should.bignumber.equal(
- await asRewardPoolBalance(oldETHBalance.add(amount))
- );
-
- // We should have zero claimed balance before ticking
- new BN(
- await dnp.darknodeBalances(darknode1, ETHEREUM)
- ).should.bignumber.equal(new BN(0));
-
- // We don't need to claim since we weren't allocated rewards last cycle
- // But claim shouldn't revert
- await dnp.claim(darknode1);
- await waitForEpoch(dnr);
-
- // We should be the only one who participated last cycle
- new BN(
- await dnr.numDarknodesPreviousEpoch()
- ).should.bignumber.equal(1);
- // We should be allocated all the rewards
- new BN(await dnp.unclaimedRewards(ETHEREUM)).should.bignumber.equal(
- newReward
- );
- const rewardShare = new BN(
- await dnp.previousCycleRewardShare(ETHEREUM)
- );
- rewardShare.should.bignumber.equal(newReward);
- const lastCycleReward = new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- );
-
- // Claim the rewards for last cycle
- await dnp.claim(darknode1);
- await waitForEpoch(dnr);
- const newPool = await asRewardPoolBalance(lastCycleReward);
- // There should be nothing left in the reward pool
- new BN(
- await dnp.currentCycleRewardPool(ETHEREUM)
- ).should.bignumber.equal(newPool);
- const earnedRewards = new BN(
- await dnp.darknodeBalances(darknode1, ETHEREUM)
- );
- earnedRewards.should.bignumber.equal(rewardShare);
-
- const oldBalance = new BN(await web3.eth.getBalance(darknode1));
- await dnp.withdraw(darknode1, ETHEREUM);
-
- // Our balances should have increased
- const newBalance = new BN(await web3.eth.getBalance(darknode1));
- newBalance.should.bignumber.equal(oldBalance.add(earnedRewards));
-
- // We should have nothing left to withdraw
- const postWithdrawRewards = new BN(
- await dnp.darknodeBalances(darknode1, ETHEREUM)
- );
- postWithdrawRewards.should.bignumber.equal(new BN(0));
-
- // Deregister ETH
- await dnp.deregisterToken(ETHEREUM);
- await waitForEpoch(dnr);
- (await dnp.registeredTokenIndex(ETHEREUM)).should.bignumber.equal(
- 0
- );
-
- const darknodePaymentMigrator = await DarknodePaymentMigrator.new(
- dnp.address,
- [ETHEREUM]
- );
-
- (await store.owner()).should.equal(dnp.address);
- await dnp.transferStoreOwnership(darknodePaymentMigrator.address);
- });
-
- it("can pay out DAI when darknodes withdraw", async () => {
- const darknode1Balance = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- darknode1Balance.gt(new BN(0)).should.be.true;
- await withdraw(darknode1);
- });
-
- it("cannot call tick twice in the same cycle", async () => {
- await dnp.claim(darknode1);
- await dnp
- .claim(darknode1)
- .should.be.rejectedWith(
- /DarknodePayment: reward already claimed/
- );
- });
-
- it("can tick again after a cycle has passed", async () => {
- await dnp.claim(darknode1);
- await waitForEpoch(dnr);
- await dnp.claim(darknode1);
- });
-
- it("should evenly split reward pool between ticked darknodes", async () => {
- const numDarknodes = 3;
- // Start from number 2 to avoid previous balances
- const startDarknode = 2;
-
- // We should only have one darknode
- new BN(
- await dnr.numDarknodesPreviousEpoch()
- ).should.bignumber.equal(1);
- // Register the darknodes
- for (let i = startDarknode; i < startDarknode + numDarknodes; i++) {
- await registerDarknode(i);
- }
- await waitForEpoch(dnr);
- // We should still only have one darknode
- new BN(
- await dnr.numDarknodesPreviousEpoch()
- ).should.bignumber.equal(1);
-
- // The darknodes should have zero balance
- for (let i = startDarknode; i < startDarknode + numDarknodes; i++) {
- const bal = new BN(
- await dnp.darknodeBalances(accounts[i], dai.address)
- );
- bal.should.bignumber.equal(new BN(0));
- // since darknode has not been around for a full epoch
- await tick(accounts[i]).should.be.rejectedWith(
- /DarknodePayment: cannot claim for this epoch/
- );
- }
-
- const rewards = new BN("300000000000000000");
- await depositDai(rewards);
-
- const rewardPool = await asRewardPoolBalance(
- await store.availableBalance(dai.address)
- );
-
- await waitForEpoch(dnr);
-
- const newRegisteredDarknodes = new BN(
- await dnr.numDarknodesPreviousEpoch()
- );
- // We should finally have increased the number of darknodes
- newRegisteredDarknodes.should.bignumber.equal(1 + numDarknodes);
- const expectedShare = rewardPool.div(newRegisteredDarknodes);
-
- await multiTick(startDarknode, numDarknodes);
- for (let i = startDarknode; i < startDarknode + numDarknodes; i++) {
- const darknodeBalance = await dnp.darknodeBalances(
- accounts[i],
- dai.address
- );
- darknodeBalance.should.bignumber.equal(expectedShare);
- }
-
- // Withdraw for each darknode
- await multiWithdraw(startDarknode, numDarknodes);
-
- // claim rewards for darknode1
- await tick(darknode1);
- });
-
- it("can call withdrawMultiple", async () => {
- // Deposit DAI and ETH
- const rewards = new BN("300000000000000000");
- await depositDai(rewards);
- await dnp.registerToken(ETHEREUM);
- await dnp.deposit(rewards, ETHEREUM, {
- value: rewards.toString(),
- from: accounts[0]
- });
-
- // Participate in rewards
- await tick(darknode1);
- // Change the epoch
- await waitForEpoch(dnr);
-
- // Claim rewards for past epoch
- await tick(darknode1);
-
- await waitForEpoch(dnr);
-
- // Claim rewards for past epoch
- await tick(darknode1);
-
- // Withdraw for each darknode
- await dnp.withdrawMultiple([darknode1], [dai.address, ETHEREUM]);
- });
-
- it("cannot withdraw if a darknode owner is invalid", async () => {
- await dnp
- .withdraw(NULL, dai.address)
- .should.be.rejectedWith(/invalid darknode owner/);
- // accounts[0] is not a registered darknode
- await dnp
- .withdraw(accounts[0], dai.address)
- .should.be.rejectedWith(/invalid darknode owner/);
- });
-
- it("cannot withdraw more than once in a cycle", async () => {
- const numDarknodes = 4;
- new BN(
- await dnr.numDarknodesPreviousEpoch()
- ).should.bignumber.equal(numDarknodes);
-
- const rewards = new BN("300000000000000000");
- await depositDai(rewards);
- await multiTick(1, numDarknodes);
- // Change the epoch
- await waitForEpoch(dnr);
-
- // Claim rewards for past cycle
- await multiTick(1, numDarknodes);
-
- // First withdraw should pass
- const balanceBefore = await dai.balanceOf(accounts[1]);
- await withdraw(darknode1);
- const balanceAfter = await dai.balanceOf(accounts[1]);
- balanceAfter.should.be.bignumber.greaterThan(balanceBefore);
-
- // Rest should fail
- await dnp.withdraw(darknode1, dai.address);
- const balanceAfterSecond = await dai.balanceOf(accounts[1]);
- balanceAfterSecond.should.be.bignumber.eq(balanceAfter);
- });
-
- it("cannot tick if it is blacklisted", async () => {
- // Should succeed if not blacklisted
- await tick(darknode2);
-
- await slasher.blacklist(darknode2);
-
- // Change the epoch
- await waitForEpoch(dnr);
-
- // Tick should fail
- await tick(darknode2).should.be.rejectedWith(
- /DarknodePayment: darknode is not registered/
- );
- });
-
- it("can still withdraw allocated rewards when blacklisted", async () => {
- // Change the epoch
- await waitForEpoch(dnr);
- // Change the epoch
- await waitForEpoch(dnr);
- // Add rewards into the next cycle's pool
- const previousBalance = new BN(
- await dnp.darknodeBalances(darknode3, dai.address)
- );
- const rewards = new BN("300000000000000000");
- await depositDai(rewards);
- // Change the epoch
- await waitForEpoch(dnr);
- const rewardPool = await asRewardPoolBalance(
- await store.availableBalance(dai.address)
- );
-
- // Claim the rewards for the pool
- await tick(darknode3);
-
- const numDarknodes = new BN(await dnr.numDarknodesPreviousEpoch());
- const rewardSplit = rewardPool.div(numDarknodes);
-
- // Claim rewards for past cycle
- await slasher.blacklist(darknode3);
-
- const newBalances = new BN(
- await dnp.darknodeBalances(darknode3, dai.address)
- );
- newBalances.should.bignumber.equal(
- previousBalance.add(rewardSplit)
- );
-
- const oldDaiBal = new BN(await dai.balanceOf(darknode3));
- await withdraw(darknode3);
- const newDaiBal = new BN(await dai.balanceOf(darknode3));
- newDaiBal.should.bignumber.equal(oldDaiBal.add(newBalances));
- });
- });
-
- describe("Transferring ownership", async () => {
- it("should disallow unauthorized transferring of ownership", async () => {
- await dnp
- .transferStoreOwnership(accounts[1], { from: accounts[1] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- await dnp
- .claimStoreOwnership({ from: accounts[1] })
- .should.be.rejectedWith(
- /Claimable: caller is not the pending owner/
- );
- });
-
- it("can transfer ownership of the darknode payment store", async () => {
- const newDarknodePayment = await DarknodePayment.new(
- "",
- dnr.address,
- store.address,
- 0
- );
-
- // [ACTION] Initiate ownership transfer to wrong account
- await dnp.transferStoreOwnership(newDarknodePayment.address, {
- from: accounts[0]
- });
-
- // [CHECK] Owner should still be dnp
- (await store.owner()).should.equal(newDarknodePayment.address);
-
- // [RESET] Initiate ownership transfer back to dnp
- await newDarknodePayment.transferStoreOwnership(dnp.address);
-
- // [CHECK] Owner should now be the dnp
- (await store.owner()).should.equal(dnp.address);
- });
- });
-
- describe("DarknodePaymentStore negative tests", async () => {
- // Transfer the ownership to owner
- before(async () => {
- const claimer = await Claimer.new(store.address);
- // [ACTION] Can correct ownership transfer
- await dnp.transferStoreOwnership(claimer.address);
- // [ACTION] Claim ownership
- await claimer.transferStoreOwnership(owner);
- await store.claimOwnership({ from: owner });
- // [CHECK] Owner should now be main account
- (await store.owner()).should.equal(owner);
- });
-
- it("cannot increment balances by an invalid amounts", async () => {
- await store
- .incrementDarknodeBalance(darknode1, dai.address, 0)
- .should.be.rejectedWith(/DarknodePaymentStore: invalid amount/);
- const invalidAmount = new BN(
- await store.availableBalance(dai.address)
- ).add(new BN(1));
- await store
- .incrementDarknodeBalance(darknode1, dai.address, invalidAmount)
- .should.be.rejectedWith(
- /DarknodePaymentStore: insufficient contract balance/
- );
- });
-
- it("cannot transfer more than is in the balance", async () => {
- const invalidAmount = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- ).add(new BN(1));
- await store
- .transfer(darknode1, dai.address, invalidAmount, darknode1)
- .should.be.rejectedWith(
- /DarknodePaymentStore: insufficient darknode balance/
- );
- });
-
- it("cannot call functions from non-owner", async () => {
- await store
- .incrementDarknodeBalance(darknode1, dai.address, new BN(0), {
- from: accounts[2]
- })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- await store
- .transfer(darknode1, dai.address, new BN(0), darknode1, {
- from: accounts[2]
- })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- await store
- .transferOwnership(dnp.address, { from: accounts[2] })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- });
-
- it("cannot transferOwnership to the same owner", async () => {
- await store
- .transferOwnership(owner, { from: owner })
- .should.be.rejectedWith(/Claimable: invalid new owner/);
- await store.transferOwnership(accounts[3], { from: owner });
- await store
- .transferOwnership(accounts[3], { from: owner })
- .should.be.rejectedWith(/Claimable: invalid new owner/);
- });
-
- // Transfer the ownership back to DNP
- after(async () => {
- // [RESET] Initiate ownership transfer back to dnp
- await store.transferOwnership(dnp.address);
- // [RESET] Claim ownership
- await dnp.claimStoreOwnership();
- // [CHECK] Owner should now be the dnp
- (await store.owner()).should.equal(dnp.address);
- });
- });
-
- describe("when updating cycle changer", async () => {
- it("cannot update cycleChanger if unauthorized", async () => {
- await dnp
- .updateCycleChanger(accounts[2], { from: accounts[2] })
- .should.be.rejectedWith(/Ownable: caller is not the owner./);
- await dnp
- .updateCycleChanger(accounts[3], { from: accounts[3] })
- .should.be.rejectedWith(/Ownable: caller is not the owner./);
- });
-
- it("cannot update cycleChanger to an invalid address", async () => {
- await dnp
- .updateCycleChanger(NULL)
- .should.be.rejectedWith(/invalid contract address/);
- });
-
- it("can update cycleChanger to different address", async () => {
- await dnp.updateCycleChanger(accounts[2]);
- await dnp.changeCycle({ from: accounts[2] });
- await dnp
- .changeCycle()
- .should.be.rejectedWith(/DarknodePayment: not cycle changer/);
- // Restore the cycle changer to the dnr address
- await dnp.updateCycleChanger(dnr.address);
- });
- });
-
- describe("when forwarding funds", async () => {
- it("can forward ETH", async () => {
- await dnp.forward(ETHEREUM);
- });
-
- it("can forward funds to the store", async () => {
- // DNP should have zero balance
- new BN(await dai.balanceOf(dnp.address)).should.bignumber.equal(
- new BN(0)
- );
-
- const storeDaiBalance = new BN(
- await store.availableBalance(dai.address)
- );
- const amount = new BN("1000000");
- new BN(await dai.balanceOf(owner)).gte(amount).should.be.true;
- await dai.transfer(dnp.address, amount);
-
- (await store.availableBalance(dai.address)).should.bignumber.equal(
- storeDaiBalance
- );
- // DNP should have some balance
- new BN(await dai.balanceOf(dnp.address)).should.bignumber.equal(
- amount
- );
-
- // Forward the funds on
- await dnp.forward(dai.address);
- new BN(await dai.balanceOf(dnp.address)).should.bignumber.equal(
- new BN(0)
- );
- (await store.availableBalance(dai.address)).should.bignumber.equal(
- storeDaiBalance.add(amount)
- );
- });
- });
-
- describe("when changing payout percent", async () => {
- it("cannot change payout percent unless authorized", async () => {
- await dnp
- .updatePayoutPercentage(new BN(10), { from: accounts[2] })
- .should.be.rejectedWith(/Ownable: caller is not the owner./);
- });
-
- it("cannot change payout percent to an invalid percent", async () => {
- await dnp
- .updatePayoutPercentage(new BN(101))
- .should.be.rejectedWith(/DarknodePayment: invalid percent/);
- await dnp
- .updatePayoutPercentage(new BN(201))
- .should.be.rejectedWith(/DarknodePayment: invalid percent/);
- await dnp
- .updatePayoutPercentage(new BN(255))
- .should.be.rejectedWith(/DarknodePayment: invalid percent/);
- await dnp
- .updatePayoutPercentage(new BN(256))
- .should.be.rejectedWith(/DarknodePayment: invalid percent/);
- await dnp
- .updatePayoutPercentage(new BN(32782))
- .should.be.rejectedWith(/DarknodePayment: invalid percent/);
- });
-
- it("can change payout percent to a valid percent", async () => {
- await updatePayoutPercent(new BN(100));
- await updatePayoutPercent(new BN(0));
- await updatePayoutPercent(new BN(10));
- await updatePayoutPercent(new BN(12));
- await updatePayoutPercent(new BN(73));
- });
-
- it("should not payout anything if payout percent is zero", async () => {
- new BN(await dnp.currentCycleRewardPool(dai.address)).gte(new BN(0))
- .should.be.true;
- const oldBal = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- await updatePayoutPercent(new BN(0));
- // current epoch payment amount is zero but previous is not
- await waitForEpoch(dnr);
- // now the current and previous payment amount should be zero
- // claiming the rewards for last epoch should be zero
- await tick(darknode1);
- const newBal = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- newBal.should.bignumber.equal(oldBal);
- });
-
- it("should payout the correct amount", async () => {
- new BN(await dnp.currentCycleRewardPool(dai.address)).gte(new BN(0))
- .should.be.true;
- const oldBal = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- const percent = new BN(20);
- await updatePayoutPercent(percent);
- // current epoch payment amount is twenty but previous is not
- await waitForEpoch(dnr);
- // now the current and previous payment amount should be twenty
- // claiming the rewards for last epoch should be twenty percent
- const rewardPool = new BN(await store.availableBalance(dai.address))
- .div(new BN(100))
- .mul(percent);
- const rewardShare = rewardPool.div(
- new BN(await dnr.numDarknodes())
- );
- await tick(darknode1);
- const newBal = new BN(
- await dnp.darknodeBalances(darknode1, dai.address)
- );
- newBal.should.bignumber.equal(oldBal.add(rewardShare));
- await updatePayoutPercent(config.DARKNODE_PAYOUT_PERCENT);
- await waitForEpoch(dnr);
- });
- });
-
- it("can update DarknodeRegistry", async () => {
- const darknodeRegistry = await dnp.darknodeRegistry();
- await dnp
- .updateDarknodeRegistry(NULL)
- .should.be.rejectedWith(
- /DarknodePayment: invalid Darknode Registry address/
- );
-
- await dnp.updateDarknodeRegistry(accounts[0]);
- await dnp.updateDarknodeRegistry(darknodeRegistry);
- });
-
- const tick = async (address: string) => {
- return dnp.claim(address);
- };
-
- const multiTick = async (start = 1, numberOfDarknodes = 1) => {
- for (let i = start; i < start + numberOfDarknodes; i++) {
- await tick(accounts[i]);
- }
- };
-
- const withdraw = async (address: string) => {
- // Our claimed amount should be positive
- const earnedDAIRewards = new BN(
- await dnp.darknodeBalances(address, dai.address)
- );
- earnedDAIRewards.gt(new BN(0)).should.be.true;
-
- const oldDAIBalance = new BN(await dai.balanceOf(address));
-
- await dnp.withdraw(address, dai.address);
-
- // Our balances should have increased
- const newDAIBalance = new BN(await dai.balanceOf(address));
- newDAIBalance.should.bignumber.equal(
- oldDAIBalance.add(earnedDAIRewards)
- );
-
- // We should have nothing left to withdraw
- const postWithdrawRewards = new BN(
- await dnp.darknodeBalances(address, dai.address)
- );
- postWithdrawRewards.should.bignumber.equal(new BN(0));
- };
-
- const multiWithdraw = async (start = 1, numberOfDarknodes = 1) => {
- for (let i = start; i < start + numberOfDarknodes; i++) {
- await withdraw(accounts[i]);
- }
- };
-
- const depositDai = async (amount: number | BN | string) => {
- const amountBN = new BN(amount);
- const previousBalance = new BN(
- await store.availableBalance(dai.address)
- );
- // Approve the contract to use DAI
- await dai.approve(dnp.address, amountBN);
- await dnp.deposit(amountBN, dai.address);
- // We should expect the DAI balance to have increased by what we deposited
- (await store.availableBalance(dai.address)).should.bignumber.equal(
- previousBalance.add(amountBN)
- );
- };
-
- const asRewardPoolBalance = async (
- amount: BN | string | number
- ): Promise => {
- const balance = new BN(amount);
- const payoutPercent = new BN(await dnp.currentCyclePayoutPercent());
- const rewardPool = balance.div(new BN(100)).mul(payoutPercent);
- return rewardPool;
- };
-
- const fetchRewardPool = async (token: string): Promise => {
- return new BN(await dnp.currentCycleRewardPool(token));
- };
-
- const registerDarknode = async (i: number) => {
- await ren.transfer(accounts[i], MINIMUM_BOND);
- await ren.approve(dnr.address, MINIMUM_BOND, { from: accounts[i] });
- // Register the darknodes under the account address
- await dnr.register(accounts[i], PUBK(i), { from: accounts[i] });
- };
-
- const updatePayoutPercent = async (percent: number | string | BN) => {
- const p = new BN(percent);
- await dnp.updatePayoutPercentage(p);
- new BN(await dnp.nextCyclePayoutPercent()).should.bignumber.equal(p);
- await waitForEpoch(dnr);
- new BN(await dnp.currentCyclePayoutPercent()).should.bignumber.equal(p);
- };
-});
diff --git a/test/DarknodeRegistry.ts b/test/DarknodeRegistry.ts
index 5778cbe4..f0ae62f7 100644
--- a/test/DarknodeRegistry.ts
+++ b/test/DarknodeRegistry.ts
@@ -1,12 +1,10 @@
import BN from "bn.js";
import {
- DarknodeRegistryLogicV1Instance,
+ DarknodeRegistryLogicV2Instance,
DarknodeRegistryStoreInstance,
- DarknodeSlasherInstance,
RenProxyAdminInstance,
RenTokenInstance,
- GetOperatorDarknodesInstance
} from "../types/truffle-contracts";
import {
deployProxy,
@@ -16,7 +14,8 @@ import {
MINIMUM_POD_SIZE,
NULL,
PUBK,
- waitForEpoch
+ signRecoverMessage,
+ waitForEpoch,
} from "./helper/testUtils";
const Claimer = artifacts.require("Claimer");
@@ -24,11 +23,9 @@ const ForceSend = artifacts.require("ForceSend");
const RenToken = artifacts.require("RenToken");
const DarknodeRegistryStore = artifacts.require("DarknodeRegistryStore");
const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const DarknodeSlasher = artifacts.require("DarknodeSlasher");
+const DarknodeRegistryLogicV2 = artifacts.require("DarknodeRegistryLogicV2");
const NormalToken = artifacts.require("NormalToken");
const RenProxyAdmin = artifacts.require("RenProxyAdmin");
-const GetOperatorDarknodes = artifacts.require("GetOperatorDarknodes");
const { config } = require("../migrations/networks");
@@ -37,18 +34,15 @@ const numAccounts = 10;
contract("DarknodeRegistry", (accounts: string[]) => {
let ren: RenTokenInstance;
let dnrs: DarknodeRegistryStoreInstance;
- let dnr: DarknodeRegistryLogicV1Instance;
- let slasher: DarknodeSlasherInstance;
+ let dnr: DarknodeRegistryLogicV2Instance;
let proxyAdmin: RenProxyAdminInstance;
before(async () => {
ren = await RenToken.deployed();
dnrs = await DarknodeRegistryStore.deployed();
const dnrProxy = await DarknodeRegistryProxy.deployed();
- dnr = await DarknodeRegistryLogicV1.at(dnrProxy.address);
- slasher = await DarknodeSlasher.deployed();
+ dnr = await DarknodeRegistryLogicV2.at(dnrProxy.address);
proxyAdmin = await RenProxyAdmin.deployed();
- await dnr.updateSlasher(slasher.address);
await dnr
.epoch({ from: accounts[1] })
.should.be.rejectedWith(
@@ -59,11 +53,15 @@ contract("DarknodeRegistry", (accounts: string[]) => {
for (let i = 1; i < numAccounts; i++) {
await ren.transfer(accounts[i], MINIMUM_BOND);
}
+
+ // Transfer accounts[numAccounts - 1] an additional MINIMUM_BOND so it can
+ // register, deregister, and refund multiple darknodes
+ await ren.transfer(accounts[numAccounts - 1], MINIMUM_BOND);
});
it("should return empty list when no darknodes are registered", async () => {
const nodes = (await dnr.getPreviousDarknodes(NULL, 100)).filter(
- x => x !== NULL
+ (x) => x !== NULL
);
nodes.length.should.equal(0);
});
@@ -100,7 +98,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
(await dnr.minimumEpochInterval()).should.bignumber.equal(0);
await dnr
.updateMinimumEpochInterval(MINIMUM_EPOCH_INTERVAL_SECONDS, {
- from: accounts[1]
+ from: accounts[1],
})
.should.be.rejectedWith(/Ownable: caller is not the owner/);
await dnr.updateMinimumEpochInterval(MINIMUM_EPOCH_INTERVAL_SECONDS);
@@ -119,12 +117,32 @@ contract("DarknodeRegistry", (accounts: string[]) => {
.should.be.rejectedWith(/ERC20: transfer amount exceeds allowance/); // failed transfer
});
+ it("cannot register multiple darknodes atomically with less than the sum of bonds", async () => {
+ const lowBond = MINIMUM_BOND.mul(new BN(2)).sub(new BN(1));
+ await ren.approve(dnr.address, lowBond, {
+ from: accounts[numAccounts - 1],
+ });
+
+ await dnr
+ .registerMultiple([ID("A"), ID("B")])
+ .should.be.rejectedWith(/ERC20: transfer amount exceeds allowance/); // failed transfer
+ });
+
it("cannot register a darknode with address zero", async () => {
+ await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(2)), {
+ from: accounts[0],
+ });
await dnr
- .register(NULL, PUBK("A"))
+ .register(NULL, PUBK("A"), { from: accounts[0] })
.should.be.rejectedWith(
/DarknodeRegistry: darknode address cannot be zero/
); // failed transfer
+
+ await dnr
+ .registerMultiple([ID("A"), NULL], { from: accounts[0] })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: darknode address cannot be zero/
+ );
});
it("can not call epoch before the minimum time interval", async () => {
@@ -136,7 +154,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
);
});
- it("can register, deregister and refund Darknodes", async function() {
+ it("can register, deregister and refund Darknodes", async function () {
this.timeout(1000 * 1000);
// [ACTION] Register
for (let i = 0; i < numAccounts; i++) {
@@ -147,7 +165,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
const nodeCount = 10;
await ren.transfer(accounts[2], MINIMUM_BOND.mul(new BN(nodeCount)));
await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(nodeCount)), {
- from: accounts[2]
+ from: accounts[2],
});
for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
@@ -157,12 +175,8 @@ contract("DarknodeRegistry", (accounts: string[]) => {
// Wait for epoch
await waitForEpoch(dnr);
- const getOperatorDarknodes = await GetOperatorDarknodes.new(
- dnr.address
- );
-
(
- await getOperatorDarknodes.getOperatorDarknodes(accounts[2])
+ await dnr.getOperatorDarknodes(accounts[2])
).length.should.bignumber.equal(nodeCount + 1); // +1 from the first loop
// [ACTION] Deregister
@@ -188,10 +202,38 @@ contract("DarknodeRegistry", (accounts: string[]) => {
}
await ren.transfer(accounts[0], MINIMUM_BOND.mul(new BN(nodeCount)), {
- from: accounts[2]
+ from: accounts[2],
});
});
+ it("can register, deregister and refund multiple Darknodes atomically", async function () {
+ this.timeout(1000 * 1000);
+ const owner = accounts[numAccounts - 1];
+
+ // [ACTION] Register
+ await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(2)), {
+ from: owner,
+ });
+ await dnr.registerMultiple([ID("0"), ID("1")], { from: owner });
+
+ // Wait for epoch
+ await waitForEpoch(dnr);
+
+ (await dnr.getOperatorDarknodes(owner)).length.should.bignumber.equal(
+ 2
+ );
+
+ // [ACTION] Deregister
+ await dnr.deregisterMultiple([ID("0"), ID("1")], { from: owner });
+
+ // Wait for two epochs
+ await waitForEpoch(dnr);
+ await waitForEpoch(dnr);
+
+ // [ACTION] Refund
+ await dnr.refundMultiple([ID("0"), ID("1")], { from: owner });
+ });
+
it("can check darknode statuses", async () => {
// WARNING: A lot of code a head
@@ -288,7 +330,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
(await dnr.isRegisteredInPreviousEpoch(id)).should.be.false;
// [ACTION] Refund
- await dnr.refund(id, { from: accounts[0] });
+ await dnr.refund(id, { from: owner });
(await dnr.isRefunded(id)).should.be.true;
(await dnr.isPendingRegistration(id)).should.be.false;
@@ -309,7 +351,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
// Approve more than minimum bond
await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(2)), {
- from: owner
+ from: owner,
});
// Register
@@ -328,18 +370,62 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await dnr.refund(id, { from: owner });
});
+ it("multiple bonds are exact multiple of minimum bond", async () => {
+ const owner = accounts[numAccounts - 1];
+
+ const renBalanceBefore = new BN(await ren.balanceOf(owner));
+
+ // Approve 3 minimum bonds
+ await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(3)), {
+ from: owner,
+ });
+
+ // Register
+ await dnr.registerMultiple([ID("0"), ID("1")], { from: owner });
+
+ // Only 2 minimum bonds should have been transferred
+ (await ren.balanceOf(owner)).should.bignumber.equal(
+ renBalanceBefore.sub(MINIMUM_BOND.mul(new BN(2)))
+ );
+
+ // [RESET]
+ await waitForEpoch(dnr);
+ await dnr.deregisterMultiple([ID("0"), ID("1")], { from: owner });
+ await waitForEpoch(dnr);
+ await waitForEpoch(dnr);
+ await dnr.refundMultiple([ID("0"), ID("1")], { from: owner });
+ });
+
it("[SETUP] Register darknodes for next tests", async () => {
- for (let i = 0; i < numAccounts; i++) {
+ // All but the last account register 1 darknode
+ for (let i = 0; i < numAccounts - 1; i++) {
await ren.approve(dnr.address, MINIMUM_BOND, { from: accounts[i] });
await dnr.register(ID(i), PUBK(i), { from: accounts[i] });
}
+
+ // Last account registers two darknodes
+ await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(2)), {
+ from: accounts[numAccounts - 1],
+ });
+ await dnr.registerMultiple([ID(numAccounts - 1), ID(numAccounts)], {
+ from: accounts[numAccounts - 1],
+ });
+
await waitForEpoch(dnr);
});
it("can not register a node twice", async () => {
- await ren.approve(dnr.address, MINIMUM_BOND, { from: accounts[0] });
+ await ren.approve(dnr.address, MINIMUM_BOND.mul(new BN(2)), {
+ from: accounts[0],
+ });
await dnr
- .register(ID("0"), PUBK("0"))
+ .register(ID("0"), PUBK("0"), { from: accounts[0] })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be refunded or never registered/
+ );
+
+ await dnr
+ .registerMultiple([ID("0"), ID("-1")])
.should.be.rejectedWith(
/DarknodeRegistry: must be refunded or never registered/
);
@@ -349,12 +435,24 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await dnr
.deregister(ID("-1"))
.should.be.rejectedWith(/DarknodeRegistry: must be deregisterable/);
+
+ // ID("0") has been registered, but not ID("-1")
+ await dnr
+ .deregisterMultiple([ID("0"), ID("-1")])
+ .should.be.rejectedWith(/DarknodeRegistry: must be deregisterable/);
});
it("only darknode owner can deregister darknode", async () => {
await dnr
.deregister(ID("0"), { from: accounts[9] })
.should.be.rejectedWith(/DarknodeRegistry: must be darknode owner/);
+
+ // accounts[9] has registered ID("9") but not ID("0")
+ await dnr
+ .deregisterMultiple([ID("9"), ID("0")], {
+ from: accounts[9],
+ })
+ .should.be.rejectedWith(/DarknodeRegistry: must be darknode owner/);
});
it("can get the owner of the Dark Node", async () => {
@@ -367,24 +465,18 @@ contract("DarknodeRegistry", (accounts: string[]) => {
);
});
- it("can get the Public Key of the Dark Node", async () => {
- (await dnr.getDarknodePublicKey(ID("0"))).should.equal(PUBK("0"));
- });
-
it("can deregister dark nodes", async () => {
await dnr.deregister(ID("0"), { from: accounts[0] });
await dnr.deregister(ID("1"), { from: accounts[1] });
await dnr.deregister(ID("4"), { from: accounts[4] });
await dnr.deregister(ID("5"), { from: accounts[5] });
await dnr.deregister(ID("8"), { from: accounts[8] });
- await dnr.deregister(ID("9"), { from: accounts[9] });
await waitForEpoch(dnr);
(await dnr.isDeregistered(ID("0"))).should.be.true;
(await dnr.isDeregistered(ID("1"))).should.be.true;
(await dnr.isDeregistered(ID("4"))).should.be.true;
(await dnr.isDeregistered(ID("5"))).should.be.true;
(await dnr.isDeregistered(ID("8"))).should.be.true;
- (await dnr.isDeregistered(ID("9"))).should.be.true;
});
it("can't deregister twice", async () => {
@@ -394,26 +486,32 @@ contract("DarknodeRegistry", (accounts: string[]) => {
});
it("can get the current epoch's registered dark nodes", async () => {
- const nodes = (await dnr.getDarknodes(NULL, 0)).filter(x => x !== NULL);
- nodes.length.should.equal(numAccounts - 6);
+ const nodes = (await dnr.getDarknodes(NULL, 0)).filter(
+ (x) => x !== NULL
+ );
+ nodes.length.should.equal(numAccounts - 4);
nodes[0].should.equal(ID("2"));
nodes[1].should.equal(ID("3"));
nodes[2].should.equal(ID("6"));
nodes[3].should.equal(ID("7"));
+ nodes[4].should.equal(ID("9"));
+ nodes[5].should.equal(ID("10"));
});
it("can get the previous epoch's registered dark nodes", async () => {
let nodes = (await dnr.getPreviousDarknodes(NULL, 0)).filter(
- x => x !== NULL
+ (x) => x !== NULL
);
- nodes.length.should.equal(numAccounts);
+
+ // The last account registered 2 darknodes
+ nodes.length.should.equal(numAccounts + 1);
await waitForEpoch(dnr);
nodes = (await dnr.getPreviousDarknodes(NULL, 0)).filter(
- x => x !== NULL
+ (x) => x !== NULL
);
- nodes.length.should.equal(numAccounts - 6);
+ nodes.length.should.equal(numAccounts - 4);
});
it("can get the dark nodes in multiple calls", async () => {
@@ -430,11 +528,13 @@ contract("DarknodeRegistry", (accounts: string[]) => {
}
} while (start !== NULL);
- nodes.length.should.equal(numAccounts - 6);
+ nodes.length.should.equal(numAccounts - 4);
nodes[0].should.equal(ID("2"));
nodes[1].should.equal(ID("3"));
nodes[2].should.equal(ID("6"));
nodes[3].should.equal(ID("7"));
+ nodes[4].should.equal(ID("9"));
+ nodes[5].should.equal(ID("10"));
});
it("can get the previous epoch's dark nodes in multiple calls", async () => {
@@ -451,11 +551,13 @@ contract("DarknodeRegistry", (accounts: string[]) => {
}
} while (start !== NULL);
- nodes.length.should.equal(numAccounts - 6);
+ nodes.length.should.equal(numAccounts - 4);
nodes[0].should.equal(ID("2"));
nodes[1].should.equal(ID("3"));
nodes[2].should.equal(ID("6"));
nodes[3].should.equal(ID("7"));
+ nodes[4].should.equal(ID("9"));
+ nodes[5].should.equal(ID("10"));
});
it("should fail to refund before deregistering", async () => {
@@ -470,11 +572,16 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await dnr.deregister(ID("3"), { from: accounts[3] });
await dnr.deregister(ID("6"), { from: accounts[6] });
await dnr.deregister(ID("7"), { from: accounts[7] });
+ await dnr.deregisterMultiple([ID("9"), ID("10")], {
+ from: accounts[numAccounts - 1],
+ });
(await dnr.isPendingDeregistration(ID("2"))).should.be.true;
(await dnr.isPendingDeregistration(ID("3"))).should.be.true;
(await dnr.isPendingDeregistration(ID("6"))).should.be.true;
(await dnr.isPendingDeregistration(ID("7"))).should.be.true;
+ (await dnr.isPendingDeregistration(ID("9"))).should.be.true;
+ (await dnr.isPendingDeregistration(ID("10"))).should.be.true;
// Call epoch
await waitForEpoch(dnr);
@@ -483,42 +590,56 @@ contract("DarknodeRegistry", (accounts: string[]) => {
(await dnr.isRegisteredInPreviousEpoch(ID("3"))).should.be.true;
(await dnr.isRegisteredInPreviousEpoch(ID("6"))).should.be.true;
(await dnr.isRegisteredInPreviousEpoch(ID("7"))).should.be.true;
+ (await dnr.isRegisteredInPreviousEpoch(ID("9"))).should.be.true;
+ (await dnr.isRegisteredInPreviousEpoch(ID("10"))).should.be.true;
(await dnr.isDeregistered(ID("2"))).should.be.true;
(await dnr.isDeregistered(ID("3"))).should.be.true;
(await dnr.isDeregistered(ID("6"))).should.be.true;
(await dnr.isDeregistered(ID("7"))).should.be.true;
+ (await dnr.isDeregistered(ID("9"))).should.be.true;
+ (await dnr.isDeregistered(ID("10"))).should.be.true;
const previousDarknodesEpoch1 = (
await dnr.getPreviousDarknodes(NULL, 0)
- ).filter(x => x !== NULL);
+ ).filter((x) => x !== NULL);
await waitForEpoch(dnr);
const previousDarknodesEpoch2 = (
await dnr.getPreviousDarknodes(NULL, 0)
- ).filter(x => x !== NULL);
+ ).filter((x) => x !== NULL);
(
previousDarknodesEpoch1.length - previousDarknodesEpoch2.length
- ).should.be.equal(4);
+ ).should.be.equal(6);
(await dnr.isDeregistered(ID("2"))).should.be.true;
(await dnr.isDeregistered(ID("3"))).should.be.true;
(await dnr.isDeregistered(ID("6"))).should.be.true;
(await dnr.isDeregistered(ID("7"))).should.be.true;
+ (await dnr.isDeregistered(ID("9"))).should.be.true;
+ (await dnr.isDeregistered(ID("10"))).should.be.true;
// Refund
await dnr.refund(ID("2"), { from: accounts[2] });
await dnr.refund(ID("3"), { from: accounts[3] });
await dnr.refund(ID("6"), { from: accounts[6] });
await dnr.refund(ID("7"), { from: accounts[7] });
+ await dnr.refundMultiple([ID("9"), ID("10")], {
+ from: accounts[numAccounts - 1],
+ });
(await dnr.isRefunded(ID("2"))).should.be.true;
(await dnr.isRefunded(ID("3"))).should.be.true;
(await dnr.isRefunded(ID("6"))).should.be.true;
(await dnr.isRefunded(ID("7"))).should.be.true;
+ (await dnr.isRefunded(ID("9"))).should.be.true;
+ (await dnr.isRefunded(ID("10"))).should.be.true;
(await ren.balanceOf(accounts[2])).should.bignumber.equal(MINIMUM_BOND);
(await ren.balanceOf(accounts[3])).should.bignumber.equal(MINIMUM_BOND);
(await ren.balanceOf(accounts[6])).should.bignumber.equal(MINIMUM_BOND);
(await ren.balanceOf(accounts[7])).should.bignumber.equal(MINIMUM_BOND);
+ (await ren.balanceOf(accounts[numAccounts - 1])).should.bignumber.equal(
+ MINIMUM_BOND.mul(new BN(2))
+ );
});
- it("anyone can refund", async () => {
+ it("only operator can refund", async () => {
const owner = accounts[2];
const id = ID("2");
const pubk = PUBK("2");
@@ -533,7 +654,10 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await waitForEpoch(dnr);
// [ACTION] Refund
- await dnr.refund(id, { from: accounts[0] });
+ await dnr
+ .refund(id, { from: accounts[0] })
+ .should.be.rejectedWith(/DarknodeRegistry: must be darknode owner/);
+ await dnr.refund(id, { from: owner });
// [CHECK] Refund was successful and bond was returned
(await dnr.isRefunded(id)).should.be.true;
@@ -561,6 +685,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
// [CHECK] Refund fails if transfer fails
await ren.pause();
await dnr.refund(ID("2")).should.be.rejectedWith(/Pausable: paused/);
+ await dnr
+ .refundMultiple([ID("2")])
+ .should.be.rejectedWith(/Pausable: paused/);
await ren.unpause();
// [RESET]
@@ -575,19 +702,8 @@ contract("DarknodeRegistry", (accounts: string[]) => {
);
});
- it("can update DarknodePayment", async () => {
- const darknodePayment = await dnr.darknodePayment();
- await dnr
- .updateDarknodePayment(NULL)
- .should.be.rejectedWith(
- /DarknodeRegistry: invalid Darknode Payment address/
- );
-
- await dnr.updateDarknodePayment(accounts[0]);
- await dnr.updateDarknodePayment(darknodePayment);
- });
-
it("cannot slash unregistered darknodes", async () => {
+ const currentSlasher = await dnr.slasher();
// Update slasher address
const newSlasher = accounts[0];
await dnr.updateSlasher(newSlasher);
@@ -599,12 +715,48 @@ contract("DarknodeRegistry", (accounts: string[]) => {
.should.be.rejectedWith(/DarknodeRegistry: invalid darknode/);
// Reset slasher address
- await dnr.updateSlasher(slasher.address);
+ await dnr.updateSlasher(currentSlasher);
await waitForEpoch(dnr);
- (await dnr.slasher()).should.equal(slasher.address);
+ (await dnr.slasher()).should.equal(currentSlasher);
+ });
+
+ it("can slash deregistered darknodes", async () => {
+ const currentSlasher = await dnr.slasher();
+ // Update slasher address
+ const newSlasher = accounts[0];
+ await dnr.updateSlasher(newSlasher);
+ await waitForEpoch(dnr);
+ (await dnr.slasher()).should.equal(newSlasher);
+
+ // Register and deregister darknode
+ await ren.approve(dnr.address, MINIMUM_BOND, {
+ from: accounts[2],
+ });
+ await dnr.register(ID("2"), PUBK("2"), {
+ from: accounts[2],
+ });
+ await waitForEpoch(dnr);
+ await dnr.deregister(ID("2"), { from: accounts[2] });
+ await waitForEpoch(dnr);
+
+ // Slash the deregistered darknode
+ await dnr.slash(ID("2"), ID("6"), 50);
+
+ // Reset slasher
+ await dnr.updateSlasher(currentSlasher);
+ await waitForEpoch(dnr);
+ (await dnr.slasher()).should.equal(currentSlasher);
+
+ // Refund darknode
+ await dnr.refund(ID("2"), { from: accounts[2] });
+ (await dnr.isRefunded(ID("2"))).should.be.true;
+
+ // Reset accounts[2]'s REN balance
+ await ren.transfer(accounts[2], MINIMUM_BOND.div(new BN(2)));
});
it("cannot slash with an invalid percent", async () => {
+ const currentSlasher = await dnr.slasher();
// Update slasher address
const newSlasher = accounts[0];
await dnr.updateSlasher(newSlasher);
@@ -627,9 +779,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
.should.be.rejectedWith(/DarknodeRegistry: invalid percent/);
// Reset slasher
- await dnr.updateSlasher(slasher.address);
+ await dnr.updateSlasher(currentSlasher);
await waitForEpoch(dnr);
- (await dnr.slasher()).should.equal(slasher.address);
+ (await dnr.slasher()).should.equal(currentSlasher);
// De-register darknode 3
await dnr.deregister(ID("2"), { from: accounts[2] });
@@ -651,13 +803,6 @@ contract("DarknodeRegistry", (accounts: string[]) => {
const newSlasher = accounts[3];
previousSlasher.should.not.equal(newSlasher);
- // [CHECK] The slasher can't be updated to 0x0
- await dnr
- .updateSlasher(NULL)
- .should.be.rejectedWith(
- /DarknodeRegistry: invalid slasher address/
- );
-
// [ACTION] Update slasher address
await dnr.updateSlasher(newSlasher);
// [CHECK] Verify the address hasn't changed before an epoch
@@ -674,8 +819,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
});
it("anyone except the slasher can not call slash", async () => {
+ const previousSlasher = await dnr.slasher();
+
// [SETUP] Set slasher to accounts[3]
- const slasherOwner = accounts[0];
const notSlasher = accounts[4];
// [SETUP] Register darknodes 3, 4, 7 and 8
@@ -698,31 +844,27 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await dnr
.slash(ID("2"), ID("6"), slashPercent, { from: notSlasher })
.should.be.rejectedWith(/DarknodeRegistry: must be slasher/);
- await dnr
- .slash(ID("2"), ID("6"), slashPercent, { from: slasherOwner })
- .should.be.rejectedWith(/DarknodeRegistry: must be slasher/);
- await slasher
- .slash(ID("2"), ID("6"), slashPercent, { from: notSlasher })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
- await slasher.slash(ID("2"), ID("6"), slashPercent, {
- from: slasherOwner
+ await dnr.updateSlasher(accounts[0]);
+ await waitForEpoch(dnr);
+
+ await dnr.slash(ID("2"), ID("6"), slashPercent, {
+ from: accounts[0],
});
- await slasher
- .slash(ID("3"), ID("6"), slashPercent, { from: slasherOwner })
- .should.be.rejectedWith(/DarknodeRegistry: invalid darknode/);
// // NOTE: The darknode doesn't prevent slashing a darknode twice
- await slasher.slash(ID("2"), ID("6"), slashPercent, {
- from: slasherOwner
+ await dnr.slash(ID("2"), ID("6"), slashPercent, {
+ from: accounts[0],
});
+
+ await dnr.updateSlasher(previousSlasher);
});
it("transfer ownership of the dark node store", async () => {
- const newDnr = await deployProxy(
+ const newDnr = await deployProxy(
web3,
DarknodeRegistryProxy,
- DarknodeRegistryLogicV1,
+ DarknodeRegistryLogicV2,
proxyAdmin.address,
[
{ type: "string", value: "test" },
@@ -732,9 +874,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
{ type: "uint256", value: config.MINIMUM_POD_SIZE },
{
type: "uint256",
- value: config.MINIMUM_EPOCH_INTERVAL_SECONDS
+ value: config.MINIMUM_EPOCH_INTERVAL_SECONDS,
},
- { type: "uint256", value: 0 }
+ { type: "uint256", value: 0 },
],
{ from: accounts[0] }
);
@@ -898,7 +1040,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
describe("when darknode payment is not set", async () => {
let newDNRstore: DarknodeRegistryStoreInstance;
- let newDNR: DarknodeRegistryLogicV1Instance;
+ let newDNR: DarknodeRegistryLogicV2Instance;
before(async () => {
// Deploy a new DNR and DNR store
@@ -906,10 +1048,10 @@ contract("DarknodeRegistry", (accounts: string[]) => {
"test",
RenToken.address
);
- newDNR = await deployProxy(
+ newDNR = await deployProxy(
web3,
DarknodeRegistryProxy,
- DarknodeRegistryLogicV1,
+ DarknodeRegistryLogicV2,
proxyAdmin.address,
[
{ type: "string", value: "test" },
@@ -919,9 +1061,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
{ type: "uint256", value: config.MINIMUM_POD_SIZE },
{
type: "uint256",
- value: config.MINIMUM_EPOCH_INTERVAL_SECONDS
+ value: config.MINIMUM_EPOCH_INTERVAL_SECONDS,
},
- { type: "uint256", value: 0 }
+ { type: "uint256", value: 0 },
],
{ from: accounts[0] }
);
@@ -935,33 +1077,10 @@ contract("DarknodeRegistry", (accounts: string[]) => {
await waitForEpoch(newDNR);
await waitForEpoch(newDNR);
});
-
- it("cannot slash", async () => {
- (await newDNR.owner()).should.equal(accounts[0]);
- const newSlasher = accounts[0];
- await newDNR.updateSlasher(newSlasher);
- await waitForEpoch(newDNR);
- (await newDNR.slasher()).should.equal(newSlasher);
-
- // We should have enough balance to register a darknode
- if (new BN(await ren.balanceOf(accounts[8])).lt(MINIMUM_BOND)) {
- await ren.transfer(accounts[8], MINIMUM_BOND);
- }
- await ren.approve(newDNR.address, MINIMUM_BOND, {
- from: accounts[8]
- });
- await newDNR.register(ID("8"), PUBK("8"), { from: accounts[8] });
- await waitForEpoch(newDNR);
- await newDNR
- .slash(ID("8"), newSlasher, new BN(10))
- .should.be.rejectedWith(
- /DarknodeRegistry: invalid payment address/
- );
- });
});
describe("upgrade DarknodeRegistry while maintaining store", async () => {
- let newDNR: DarknodeRegistryLogicV1Instance;
+ let newDNR: DarknodeRegistryLogicV2Instance;
let preCountPreviousEpoch: BN;
let preCount: BN;
@@ -984,10 +1103,10 @@ contract("DarknodeRegistry", (accounts: string[]) => {
preDarknodes = await dnr.getDarknodes(NULL, 0);
// Deploy a new DNR and DNR store
- newDNR = await deployProxy(
+ newDNR = await deployProxy(
web3,
DarknodeRegistryProxy,
- DarknodeRegistryLogicV1,
+ DarknodeRegistryLogicV2,
proxyAdmin.address,
[
{ type: "string", value: "test" },
@@ -997,9 +1116,9 @@ contract("DarknodeRegistry", (accounts: string[]) => {
{ type: "uint256", value: config.MINIMUM_POD_SIZE },
{
type: "uint256",
- value: config.MINIMUM_EPOCH_INTERVAL_SECONDS
+ value: config.MINIMUM_EPOCH_INTERVAL_SECONDS,
},
- { type: "uint256", value: 0 }
+ { type: "uint256", value: 0 },
],
{ from: accounts[0] }
);
@@ -1018,7 +1137,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
new BN(
(await dnr.numDarknodes()).toString()
- ).should.bignumber.equal(preCount.add(new BN(1)));
+ ).should.bignumber.equal(preCount.add(new BN(2)));
});
it("number of darknodes is correct", async () => {
@@ -1036,18 +1155,25 @@ contract("DarknodeRegistry", (accounts: string[]) => {
countNextEpoch.should.bignumber.equal(preCountNextEpoch);
darknodes.should.deep.equal(preDarknodes);
- await ren.approve(newDNR.address, MINIMUM_BOND);
- await newDNR.register(ID("10"), PUBK("10"));
+ await ren.approve(newDNR.address, MINIMUM_BOND.mul(new BN(2)), {
+ from: accounts[numAccounts - 1],
+ });
+ await newDNR.register(ID("10"), PUBK("10"), {
+ from: accounts[numAccounts - 1],
+ });
+ await newDNR.registerMultiple([ID("11")], {
+ from: accounts[numAccounts - 1],
+ });
new BN(
(await newDNR.numDarknodesNextEpoch()).toString()
- ).should.bignumber.equal(countNextEpoch.add(new BN(1)));
+ ).should.bignumber.equal(countNextEpoch.add(new BN(2)));
await waitForEpoch(newDNR);
new BN(
(await newDNR.numDarknodes()).toString()
- ).should.bignumber.equal(count.add(new BN(1)));
+ ).should.bignumber.equal(count.add(new BN(2)));
});
});
@@ -1062,7 +1188,7 @@ contract("DarknodeRegistry", (accounts: string[]) => {
to: accounts[0],
from: accounts[i],
value: balance,
- gasPrice: 0
+ gasPrice: 0,
});
}
@@ -1105,4 +1231,261 @@ contract("DarknodeRegistry", (accounts: string[]) => {
console.debug("");
});
+
+ describe("recover", () => {
+ it("should be able to recover a darknode's bond", async function () {
+ this.timeout(1000 * 1000);
+
+ // [ACTION] Register
+ for (let i = 0; i < numAccounts; i++) {
+ await ren.approve(dnr.address, MINIMUM_BOND, {
+ from: accounts[i],
+ });
+ await dnr.register(ID(i), PUBK(i), { from: accounts[i] });
+ }
+
+ const nodeCount = 10;
+ await ren.transfer(
+ accounts[2],
+ MINIMUM_BOND.mul(new BN(nodeCount))
+ );
+ await ren.approve(
+ dnr.address,
+ MINIMUM_BOND.mul(new BN(nodeCount)),
+ {
+ from: accounts[2],
+ }
+ );
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ await dnr.register(ID(i), PUBK(i), { from: accounts[2] });
+ }
+
+ // Wait for epoch
+ await waitForEpoch(dnr);
+
+ (
+ await dnr.getOperatorDarknodes(accounts[2])
+ ).length.should.bignumber.equal(nodeCount + 1); // +1 from the first loop
+
+ const recovered: number[] = [];
+
+ // [ACTION] Deregister
+ for (let i = 0; i < numAccounts; i++) {
+ await dnr.deregister(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ if (recovered.indexOf(i) >= 0) {
+ continue;
+ }
+ await dnr.deregister(ID(i), { from: accounts[2] });
+ }
+
+ // Wait for two epochs
+ await waitForEpoch(dnr);
+
+ // Recover
+ const darknodeOperator = accounts[2];
+ const recipient = accounts[3];
+ const recipientBalanceBefore = await ren.balanceOf(recipient);
+ const darknodeToRefund = numAccounts;
+ (await dnr.isDeregistered(ID(darknodeToRefund))).should.be.true;
+ (await dnr.isRefundable(ID(darknodeToRefund))).should.be.false;
+ (await dnr.getDarknodeOperator(ID(darknodeToRefund))).should.equal(
+ darknodeOperator
+ );
+
+ const signature = await signRecoverMessage(
+ darknodeOperator,
+ recipient,
+ ID(darknodeToRefund)
+ );
+ await dnr.recover(ID(darknodeToRefund), recipient, signature, {
+ from: accounts[0],
+ });
+ recovered.push(darknodeToRefund);
+
+ (await dnr.isDeregistered(ID(darknodeToRefund))).should.be.false;
+ (await dnr.isRefundable(ID(darknodeToRefund))).should.be.false;
+
+ // Can't re-use signature
+ await dnr
+ .recover(ID(darknodeToRefund), recipient, signature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ const recipientBalanceAfter = await ren.balanceOf(recipient);
+ recipientBalanceAfter
+ .sub(recipientBalanceBefore)
+ .should.bignumber.equal(await dnr.minimumBond());
+ await ren.transfer(darknodeOperator, await dnr.minimumBond(), {
+ from: recipient,
+ });
+
+ await waitForEpoch(dnr);
+
+ // Recover;
+ (await dnr.isDeregistered(ID(darknodeToRefund + 1))).should.be.true;
+ (await dnr.isRefundable(ID(darknodeToRefund + 1))).should.be.true;
+ (
+ await dnr.getDarknodeOperator(ID(darknodeToRefund + 1))
+ ).should.equal(darknodeOperator);
+
+ const signatureThree = await signRecoverMessage(
+ darknodeOperator,
+ recipient,
+ ID(darknodeToRefund + 1)
+ );
+ await dnr.recover(
+ ID(darknodeToRefund + 1),
+ recipient,
+ signatureThree,
+ {
+ from: accounts[0],
+ }
+ );
+ recovered.push(darknodeToRefund + 1);
+
+ (await dnr.isDeregistered(ID(darknodeToRefund + 1))).should.be
+ .false;
+ (await dnr.isRefundable(ID(darknodeToRefund + 1))).should.be.false;
+
+ // [ACTION] Refund
+ for (let i = 0; i < numAccounts; i++) {
+ await dnr.refund(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ if (recovered.indexOf(i) >= 0) {
+ continue;
+ }
+ await dnr.refund(ID(i), { from: accounts[2] });
+ }
+
+ const signatureFour = await signRecoverMessage(
+ darknodeOperator,
+ recipient,
+ ID(darknodeToRefund + 2)
+ );
+ await dnr
+ .recover(ID(darknodeToRefund + 2), recipient, signatureFour, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await ren.transfer(
+ accounts[0],
+ MINIMUM_BOND.mul(new BN(nodeCount)),
+ {
+ from: accounts[2],
+ }
+ );
+ });
+
+ it("can't recover with invalid signature or caller", async function () {
+ this.timeout(1000 * 1000);
+
+ // Approve more than minimum bond
+ await ren.approve(dnr.address, MINIMUM_BOND, {
+ from: accounts[1],
+ });
+
+ const darknodeToRefund = numAccounts;
+ const goodSignature = await signRecoverMessage(
+ accounts[2],
+ accounts[2],
+ ID(darknodeToRefund)
+ );
+
+ const badSignature1 = await signRecoverMessage(
+ accounts[3],
+ accounts[2],
+ ID(darknodeToRefund)
+ );
+ const badSignature2 = await signRecoverMessage(
+ accounts[2],
+ accounts[2],
+ ID(darknodeToRefund + 1)
+ );
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ // Register
+ await dnr.register(ID("1"), PUBK("1"), { from: accounts[1] });
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[2],
+ })
+ .should.be.rejectedWith(/Ownable: caller is not the owner/);
+
+ await waitForEpoch(dnr);
+ await dnr.deregister(ID("1"), { from: accounts[1] });
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await waitForEpoch(dnr);
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], badSignature1, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], badSignature2, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+
+ await waitForEpoch(dnr);
+ await dnr.refund(ID("1"), { from: accounts[1] });
+
+ await dnr
+ .recover(ID(darknodeToRefund), accounts[2], goodSignature, {
+ from: accounts[0],
+ })
+ .should.be.rejectedWith(
+ /DarknodeRegistry: must be deregistered/
+ );
+ });
+ });
});
diff --git a/test/DarknodeRegistryMigration.ts b/test/DarknodeRegistryMigration.ts
new file mode 100644
index 00000000..b4f90aeb
--- /dev/null
+++ b/test/DarknodeRegistryMigration.ts
@@ -0,0 +1,444 @@
+import BN from "bn.js";
+
+import {
+ DarknodeRegistryLogicV1Instance,
+ DarknodeRegistryLogicV2Instance,
+ DarknodeRegistryStoreInstance,
+ DarknodeRegistryV1ToV2UpgraderInstance,
+ RenProxyAdminInstance,
+ RenTokenInstance,
+} from "../types/truffle-contracts";
+import {
+ encodeCallData,
+ ID,
+ MINIMUM_BOND,
+ PUBK,
+ signRecoverMessage,
+ waitForEpoch,
+} from "./helper/testUtils";
+
+const RenToken = artifacts.require("RenToken");
+const DarknodeRegistryStore = artifacts.require("DarknodeRegistryStore");
+const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
+const DarknodeRegistryLogicV2 = artifacts.require("DarknodeRegistryLogicV2");
+const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
+const RenProxyAdmin = artifacts.require("RenProxyAdmin");
+const DarknodeRegistryV1ToV2Upgrader = artifacts.require(
+ "DarknodeRegistryV1ToV2Upgrader"
+);
+
+const { config } = require("../migrations/networks");
+
+const numAccounts = 10;
+
+contract.only("DarknodeRegistryV1ToV2Upgrader", (accounts: string[]) => {
+ let ren: RenTokenInstance;
+ let dnrV1: DarknodeRegistryLogicV1Instance;
+ let dnrV2: DarknodeRegistryLogicV2Instance;
+ let darknodeRegistryLogicV1: DarknodeRegistryLogicV1Instance;
+ let darknodeRegistryLogicV2: DarknodeRegistryLogicV2Instance;
+ let renProxyAdmin: RenProxyAdminInstance;
+ let upgrader: DarknodeRegistryV1ToV2UpgraderInstance;
+
+ before(async () => {
+ ren = await RenToken.deployed();
+ renProxyAdmin = await RenProxyAdmin.deployed();
+
+ const dnrs = await DarknodeRegistryStore.new("1", RenToken.address);
+
+ darknodeRegistryLogicV1 = await DarknodeRegistryLogicV1.new();
+ darknodeRegistryLogicV2 = await DarknodeRegistryLogicV2.new();
+ const darknodeRegistryParameters = {
+ types: [
+ "string",
+ "address",
+ "address",
+ "uint256",
+ "uint256",
+ "uint256",
+ "uint256",
+ ],
+ values: [
+ "1",
+ RenToken.address,
+ dnrs.address,
+ config.MINIMUM_BOND.toString(),
+ config.MINIMUM_POD_SIZE,
+ config.MINIMUM_EPOCH_INTERVAL_SECONDS,
+ 0,
+ ],
+ };
+
+ const darknodeRegistryProxy = await DarknodeRegistryProxy.new();
+ await darknodeRegistryProxy.methods[
+ "initialize(address,address,bytes)"
+ ](
+ darknodeRegistryLogicV1.address,
+ renProxyAdmin.address,
+ encodeCallData(
+ web3,
+ "initialize",
+ darknodeRegistryParameters.types,
+ darknodeRegistryParameters.values
+ )
+ );
+ dnrV1 = await DarknodeRegistryLogicV1.at(darknodeRegistryProxy.address);
+ dnrV2 = await DarknodeRegistryLogicV2.at(darknodeRegistryProxy.address);
+
+ await dnrs.transferOwnership(dnrV1.address);
+ await dnrV1.claimStoreOwnership();
+
+ await waitForEpoch(dnrV1);
+ await waitForEpoch(dnrV1);
+
+ for (let i = 1; i < numAccounts; i++) {
+ await ren.transfer(accounts[i], MINIMUM_BOND);
+ }
+
+ // Transfer accounts[numAccounts - 1] an additional MINIMUM_BOND so it can
+ // register, deregister, and refund multiple darknodes
+ await ren.transfer(accounts[numAccounts - 1], MINIMUM_BOND);
+
+ upgrader = await DarknodeRegistryV1ToV2Upgrader.new(
+ renProxyAdmin.address,
+ darknodeRegistryProxy.address,
+ darknodeRegistryLogicV2.address,
+ { from: accounts[0] }
+ );
+ });
+
+ it("can register, deregister and refund Darknodes", async function () {
+ this.timeout(1000 * 1000);
+ // [ACTION] Register
+ for (let i = 0; i < numAccounts; i++) {
+ await ren.approve(dnrV1.address, MINIMUM_BOND, {
+ from: accounts[i],
+ });
+ await dnrV1.register(ID(i), PUBK(i), { from: accounts[i] });
+ }
+
+ const nodeCount = 10;
+ await ren.transfer(accounts[2], MINIMUM_BOND.mul(new BN(nodeCount)));
+ await ren.approve(dnrV1.address, MINIMUM_BOND.mul(new BN(nodeCount)), {
+ from: accounts[2],
+ });
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ await dnrV1.register(ID(i), PUBK(i), { from: accounts[2] });
+ }
+
+ // Wait for epoch
+ await waitForEpoch(dnrV1);
+
+ // [ACTION] Deregister
+ for (let i = 0; i < numAccounts; i++) {
+ await dnrV1.deregister(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ await dnrV1.deregister(ID(i), { from: accounts[2] });
+ }
+
+ // Wait for two epochs
+ await waitForEpoch(dnrV1);
+ await waitForEpoch(dnrV1);
+
+ // [ACTION] Refund
+ for (let i = 0; i < numAccounts; i++) {
+ await dnrV1.refund(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ await dnrV1.refund(ID(i), { from: accounts[2] });
+ }
+
+ await ren.transfer(accounts[0], MINIMUM_BOND.mul(new BN(nodeCount)), {
+ from: accounts[2],
+ });
+ });
+
+ // it("can upgrade", async function () {
+ // /** **** UPGRADE **** */
+ // await dnrV1.transferOwnership(upgrader.address, { from: accounts[0] });
+ // await renProxyAdmin.transferOwnership(upgrader.address, {
+ // from: accounts[0],
+ // });
+
+ // await upgrader.upgrade({ from: accounts[0] });
+
+ // await upgrader.returnDNR();
+ // await upgrader.returnProxyAdmin();
+ // /** **** **** */
+ // });
+
+ // it("should be able to recover a darknode's bond", async function () {
+ // this.timeout(1000 * 1000);
+
+ // // [ACTION] Register
+ // for (let i = 0; i < numAccounts; i++) {
+ // await ren.approve(dnrV2.address, MINIMUM_BOND, {
+ // from: accounts[i],
+ // });
+ // await dnrV2.register(ID(i), PUBK(i), { from: accounts[i] });
+ // }
+
+ // const nodeCount = 10;
+ // await ren.transfer(accounts[2], MINIMUM_BOND.mul(new BN(nodeCount)));
+ // await ren.approve(dnrV2.address, MINIMUM_BOND.mul(new BN(nodeCount)), {
+ // from: accounts[2],
+ // });
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // await dnrV2.register(ID(i), PUBK(i), { from: accounts[2] });
+ // }
+
+ // // Wait for epoch
+ // await waitForEpoch(dnrV2);
+
+ // (
+ // await dnrV2.getOperatorDarknodes(accounts[2])
+ // ).length.should.bignumber.equal(nodeCount + 1); // +1 from the first loop
+
+ // const recovered: number[] = [];
+
+ // // [ACTION] Deregister
+ // for (let i = 0; i < numAccounts; i++) {
+ // await dnrV2.deregister(ID(i), { from: accounts[i] });
+ // }
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // if (recovered.indexOf(i) >= 0) {
+ // continue;
+ // }
+ // await dnrV2.deregister(ID(i), { from: accounts[2] });
+ // }
+
+ // // Wait for two epochs
+ // await waitForEpoch(dnrV2);
+
+ // // Recover
+ // const darknodeOperator = accounts[2];
+ // const recipient = accounts[3];
+ // const recipientBalanceBefore = await ren.balanceOf(recipient);
+ // const darknodeToRefund = numAccounts;
+ // const signature = await signRecoverMessage(
+ // darknodeOperator,
+ // recipient,
+ // ID(darknodeToRefund)
+ // );
+ // await dnrV2.recover(ID(darknodeToRefund), recipient, signature, {
+ // from: accounts[0],
+ // });
+ // recovered.push(darknodeToRefund);
+
+ // const recipientBalanceAfter = await ren.balanceOf(recipient);
+ // recipientBalanceAfter
+ // .sub(recipientBalanceBefore)
+ // .should.bignumber.equal(await dnrV2.minimumBond());
+ // await ren.transfer(darknodeOperator, await dnrV2.minimumBond(), {
+ // from: recipient,
+ // });
+ // (
+ // await dnrV2.getOperatorDarknodes(accounts[2])
+ // ).length.should.bignumber.equal(nodeCount);
+
+ // await waitForEpoch(dnrV2);
+
+ // // Recover
+ // const signatureThree = await signRecoverMessage(
+ // darknodeOperator,
+ // recipient,
+ // ID(darknodeToRefund + 1)
+ // );
+ // await dnrV2.recover(
+ // ID(darknodeToRefund + 1),
+ // recipient,
+ // signatureThree,
+ // {
+ // from: accounts[0],
+ // }
+ // );
+ // recovered.push(darknodeToRefund + 1);
+
+ // // [ACTION] Refund
+ // for (let i = 0; i < numAccounts; i++) {
+ // await dnrV2.refund(ID(i), { from: accounts[i] });
+ // }
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // if (recovered.indexOf(i) >= 0) {
+ // continue;
+ // }
+ // await dnrV2.refund(ID(i), { from: accounts[2] });
+ // }
+
+ // await ren.transfer(accounts[0], MINIMUM_BOND.mul(new BN(nodeCount)), {
+ // from: accounts[2],
+ // });
+ // });
+
+ it("can upgrade", async function () {
+ this.timeout(1000 * 1000);
+
+ // [ACTION] Register
+ for (let i = 0; i < numAccounts; i++) {
+ await ren.approve(dnrV1.address, MINIMUM_BOND, {
+ from: accounts[i],
+ });
+ await dnrV1.register(ID(i), PUBK(i), { from: accounts[i] });
+ }
+
+ const nodeCount = 10;
+ await ren.transfer(accounts[2], MINIMUM_BOND.mul(new BN(nodeCount)));
+ await ren.approve(dnrV1.address, MINIMUM_BOND.mul(new BN(nodeCount)), {
+ from: accounts[2],
+ });
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ await dnrV1.register(ID(i), PUBK(i), { from: accounts[2] });
+ }
+
+ // Wait for epoch
+ await waitForEpoch(dnrV1);
+
+ const recovered: number[] = [];
+
+ // [ACTION] Deregister
+ for (let i = 0; i < numAccounts; i++) {
+ await dnrV1.deregister(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ if (recovered.indexOf(i) >= 0) {
+ continue;
+ }
+ await dnrV1.deregister(ID(i), { from: accounts[2] });
+ }
+
+ // Wait for two epochs
+ await waitForEpoch(dnrV1);
+
+ /** **** UPGRADE **** */
+ await dnrV1.transferOwnership(upgrader.address, { from: accounts[0] });
+ await renProxyAdmin.transferOwnership(upgrader.address, {
+ from: accounts[0],
+ });
+
+ await upgrader.upgrade({ from: accounts[0] });
+ /** **** **** */
+
+ // Recover
+ const darknodeOperator = accounts[2];
+ const recipient = accounts[3];
+ const recipientBalanceBefore = await ren.balanceOf(recipient);
+ const darknodeToRefund = numAccounts;
+ const signature = await signRecoverMessage(
+ darknodeOperator,
+ recipient,
+ ID(darknodeToRefund)
+ );
+ await upgrader.recover(ID(darknodeToRefund), recipient, signature, {
+ from: accounts[0],
+ });
+ recovered.push(darknodeToRefund);
+
+ await upgrader.returnDNR();
+ await upgrader.returnProxyAdmin();
+
+ const recipientBalanceAfter = await ren.balanceOf(recipient);
+ recipientBalanceAfter
+ .sub(recipientBalanceBefore)
+ .should.bignumber.equal(await dnrV2.minimumBond());
+ await ren.transfer(darknodeOperator, await dnrV2.minimumBond(), {
+ from: recipient,
+ });
+
+ await waitForEpoch(dnrV2);
+
+ // Recover
+ const signatureThree = await signRecoverMessage(
+ darknodeOperator,
+ recipient,
+ ID(darknodeToRefund + 1)
+ );
+ await dnrV2.recover(
+ ID(darknodeToRefund + 1),
+ recipient,
+ signatureThree,
+ {
+ from: accounts[0],
+ }
+ );
+ recovered.push(darknodeToRefund + 1);
+
+ // [ACTION] Refund
+ for (let i = 0; i < numAccounts; i++) {
+ await dnrV2.refund(ID(i), { from: accounts[i] });
+ }
+
+ for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ if (recovered.indexOf(i) >= 0) {
+ continue;
+ }
+ await dnrV2.refund(ID(i), { from: accounts[2] });
+ }
+
+ await ren.transfer(accounts[0], MINIMUM_BOND.mul(new BN(nodeCount)), {
+ from: accounts[2],
+ });
+ });
+
+ // it("can register, deregister and refund Darknodes", async function () {
+ // this.timeout(1000 * 1000);
+ // // [ACTION] Register
+ // for (let i = 0; i < numAccounts; i++) {
+ // await ren.approve(dnrV2.address, MINIMUM_BOND, {
+ // from: accounts[i],
+ // });
+ // await dnrV2.register(ID(i), PUBK(i), { from: accounts[i] });
+ // }
+
+ // const nodeCount = 10;
+ // await ren.transfer(accounts[2], MINIMUM_BOND.mul(new BN(nodeCount)));
+ // await ren.approve(dnrV2.address, MINIMUM_BOND.mul(new BN(nodeCount)), {
+ // from: accounts[2],
+ // });
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // await dnrV2.register(ID(i), PUBK(i), { from: accounts[2] });
+ // }
+
+ // // Wait for epoch
+ // await waitForEpoch(dnrV2);
+
+ // (
+ // await dnrV2.getOperatorDarknodes(accounts[2])
+ // ).length.should.bignumber.equal(nodeCount + 1); // +1 from the first loop
+
+ // // [ACTION] Deregister
+ // for (let i = 0; i < numAccounts; i++) {
+ // await dnrV2.deregister(ID(i), { from: accounts[i] });
+ // }
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // await dnrV2.deregister(ID(i), { from: accounts[2] });
+ // }
+
+ // // Wait for two epochs
+ // await waitForEpoch(dnrV2);
+ // await waitForEpoch(dnrV2);
+
+ // // [ACTION] Refund
+ // for (let i = 0; i < numAccounts; i++) {
+ // await dnrV2.refund(ID(i), { from: accounts[i] });
+ // }
+
+ // for (let i = numAccounts; i < numAccounts + nodeCount; i++) {
+ // await dnrV2.refund(ID(i), { from: accounts[2] });
+ // }
+
+ // await ren.transfer(accounts[0], MINIMUM_BOND.mul(new BN(nodeCount)), {
+ // from: accounts[2],
+ // });
+ // });
+});
diff --git a/test/DarknodeSlasher.ts b/test/DarknodeSlasher.ts
deleted file mode 100644
index 26c80d1d..00000000
--- a/test/DarknodeSlasher.ts
+++ /dev/null
@@ -1,764 +0,0 @@
-import BN from "bn.js";
-import { ecsign } from "ethereumjs-util";
-import hashjs from "hash.js";
-
-// import { config } from "../migrations/networks";
-import {
- DarknodeRegistryLogicV1Instance,
- DarknodeSlasherInstance,
- RenTokenInstance
-} from "../types/truffle-contracts";
-import { MINIMUM_BOND, NULL, Ox, PUBK, waitForEpoch } from "./helper/testUtils";
-import {
- Darknode,
- generatePrecommitMessage,
- generatePrevoteMessage,
- generateProposeMessage,
- generateSecretMessage
-} from "./Validate";
-
-const RenToken = artifacts.require("RenToken");
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const DarknodeSlasher = artifacts.require("DarknodeSlasher");
-
-const numDarknodes = 5;
-
-contract("DarknodeSlasher", (accounts: string[]) => {
- let ren: RenTokenInstance;
- let dnr: DarknodeRegistryLogicV1Instance;
- let slasher: DarknodeSlasherInstance;
- const darknodes = new Array();
-
- const owner = accounts[0];
-
- before(async () => {
- ren = await RenToken.deployed();
- const dnrProxy = await DarknodeRegistryProxy.deployed();
- dnr = await DarknodeRegistryLogicV1.at(dnrProxy.address);
- slasher = await DarknodeSlasher.deployed();
- await dnr.updateSlasher(slasher.address);
- await waitForEpoch(dnr);
-
- for (let i = 0; i < numDarknodes; i++) {
- const darknode = web3.eth.accounts.create();
- const privKey = Buffer.from(darknode.privateKey.slice(2), "hex");
-
- // top up the darknode address with 1 ETH
- await web3.eth.sendTransaction({
- to: darknode.address,
- from: owner,
- value: web3.utils.toWei("1")
- });
- await web3.eth.personal.importRawKey(darknode.privateKey, "");
- await web3.eth.personal.unlockAccount(darknode.address, "", 6000);
-
- // transfer ren and register darknode
- await ren.transfer(darknode.address, MINIMUM_BOND);
- await ren.approve(dnr.address, MINIMUM_BOND, {
- from: darknode.address
- });
- // Register the darknodes under the account address
- await dnr.register(darknode.address, PUBK(darknode.address), {
- from: darknode.address
- });
- darknodes.push({
- account: darknode,
- privateKey: privKey
- });
- }
- await waitForEpoch(dnr);
- });
-
- describe("when setting percentages", async () => {
- it("can set a valid blacklist percentage", async () => {
- const p1 = new BN("1");
- await slasher.setBlacklistSlashPercent(p1);
- (await slasher.blacklistSlashPercent()).should.bignumber.equal(p1);
- const p2 = new BN("10");
- await slasher.setBlacklistSlashPercent(p2);
- (await slasher.blacklistSlashPercent()).should.bignumber.equal(p2);
- const p3 = new BN("12");
- await slasher.setBlacklistSlashPercent(p3);
- (await slasher.blacklistSlashPercent()).should.bignumber.equal(p3);
- });
-
- it("can set a valid malicious percentage", async () => {
- const p1 = new BN("1");
- await slasher.setMaliciousSlashPercent(p1);
- (await slasher.maliciousSlashPercent()).should.bignumber.equal(p1);
- const p2 = new BN("10");
- await slasher.setMaliciousSlashPercent(p2);
- (await slasher.maliciousSlashPercent()).should.bignumber.equal(p2);
- const p3 = new BN("12");
- await slasher.setMaliciousSlashPercent(p3);
- (await slasher.maliciousSlashPercent()).should.bignumber.equal(p3);
- });
-
- it("can set a valid secret reveal percentage", async () => {
- const p1 = new BN("1");
- await slasher.setSecretRevealSlashPercent(p1);
- (await slasher.secretRevealSlashPercent()).should.bignumber.equal(
- p1
- );
- const p2 = new BN("10");
- await slasher.setSecretRevealSlashPercent(p2);
- (await slasher.secretRevealSlashPercent()).should.bignumber.equal(
- p2
- );
- const p3 = new BN("12");
- await slasher.setSecretRevealSlashPercent(p3);
- (await slasher.secretRevealSlashPercent()).should.bignumber.equal(
- p3
- );
- });
-
- it("cannot set an invalid blacklist percentage", async () => {
- await slasher
- .setBlacklistSlashPercent(new BN("1001"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setBlacklistSlashPercent(new BN("101"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setBlacklistSlashPercent(new BN("1234"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- });
-
- it("cannot set an invalid malicious percentage", async () => {
- await slasher
- .setMaliciousSlashPercent(new BN("1001"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setMaliciousSlashPercent(new BN("101"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setMaliciousSlashPercent(new BN("1234"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- });
-
- it("cannot set an invalid secret reveal percentage", async () => {
- await slasher
- .setSecretRevealSlashPercent(new BN("1001"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setSecretRevealSlashPercent(new BN("101"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- await slasher
- .setSecretRevealSlashPercent(new BN("1234"))
- .should.be.rejectedWith(/DarknodeSlasher: invalid percentage/);
- });
- });
-
- describe("when blacklisting", async () => {
- it("cannot blacklist twice", async () => {
- await slasher.blacklist(darknodes[4].account.address);
- await slasher
- .blacklist(darknodes[4].account.address)
- .should.be.rejectedWith(/DarknodeSlasher: already blacklisted/);
- });
- });
-
- describe("when the signatures are the same", async () => {
- it("should not slash identical propose messages", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const validRound1 = new BN("6345888412984379713");
- const proposeMsg1 = generateProposeMessage(
- height,
- round,
- blockhash1,
- validRound1
- );
- const hash1 = hashjs
- .sha256()
- .update(proposeMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- await slasher
- .slashDuplicatePropose(
- height,
- round,
- hexBlockhash1,
- validRound1,
- sigString1,
- hexBlockhash1,
- validRound1,
- sigString1
- )
- .should.be.rejectedWith(/Validate: same signature/);
- });
-
- it("should not slash identical prevote messages", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const prevoteMsg1 = generatePrevoteMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(prevoteMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- await slasher
- .slashDuplicatePrevote(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash1,
- sigString1
- )
- .should.be.rejectedWith(/Validate: same signature/);
- });
-
- it("should not slash identical precommit messages", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const precommitMsg1 = generatePrecommitMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(precommitMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- await slasher
- .slashDuplicatePrecommit(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash1,
- sigString1
- )
- .should.be.rejectedWith(/Validate: same signature/);
- });
- });
-
- describe("when the signers are different", async () => {
- it("should not slash for propose messages", async () => {
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const validRound1 = new BN("6345888412984379713");
- const proposeMsg1 = generateProposeMessage(
- height,
- round,
- blockhash1,
- validRound1
- );
- const hash1 = hashjs
- .sha256()
- .update(proposeMsg1)
- .digest("hex");
- const sig1 = ecsign(
- Buffer.from(hash1, "hex"),
- darknodes[0].privateKey
- );
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const validRound2 = new BN("5327204637322492082");
- const proposeMsg2 = generateProposeMessage(
- height,
- round,
- blockhash2,
- validRound2
- );
- const hash2 = hashjs
- .sha256()
- .update(proposeMsg2)
- .digest("hex");
- const sig2 = ecsign(
- Buffer.from(hash2, "hex"),
- darknodes[1].privateKey
- );
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- // first slash should pass
- await slasher
- .slashDuplicatePropose(
- height,
- round,
- hexBlockhash1,
- validRound1,
- sigString1,
- hexBlockhash2,
- validRound2,
- sigString2
- )
- .should.be.rejectedWith(/Validate: different signer/);
- });
-
- it("should not slash for prevote messages", async () => {
- const height = new BN("6349363483468961232");
- const round = new BN("3652348943894236532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const prevoteMsg1 = generatePrevoteMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(prevoteMsg1)
- .digest("hex");
- const sig1 = ecsign(
- Buffer.from(hash1, "hex"),
- darknodes[0].privateKey
- );
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const prevoteMsg2 = generatePrevoteMessage(
- height,
- round,
- blockhash2
- );
- const hash2 = hashjs
- .sha256()
- .update(prevoteMsg2)
- .digest("hex");
- const sig2 = ecsign(
- Buffer.from(hash2, "hex"),
- darknodes[1].privateKey
- );
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- // first slash should pass
- await slasher
- .slashDuplicatePrevote(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2
- )
- .should.be.rejectedWith(/Validate: different signer/);
- });
-
- it("should not slash for precommit messages", async () => {
- const height = new BN("6348943938419561232");
- const round = new BN("3652348939484336532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const precommitMsg1 = generatePrecommitMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(precommitMsg1)
- .digest("hex");
- const sig1 = ecsign(
- Buffer.from(hash1, "hex"),
- darknodes[0].privateKey
- );
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const precommitMsg2 = generatePrecommitMessage(
- height,
- round,
- blockhash2
- );
- const hash2 = hashjs
- .sha256()
- .update(precommitMsg2)
- .digest("hex");
- const sig2 = ecsign(
- Buffer.from(hash2, "hex"),
- darknodes[1].privateKey
- );
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- // first slash should pass
- await slasher
- .slashDuplicatePrecommit(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2
- )
- .should.be.rejectedWith(/Validate: different signer/);
- });
- });
-
- describe("when malicious messages are received", async () => {
- it("should slash duplicate proposals for the same height and round", async () => {
- const darknode = darknodes[0];
- const height = new BN("6343893498349561232");
- const round = new BN("3652348943983436532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const validRound1 = new BN("6345888412984379713");
- const proposeMsg1 = generateProposeMessage(
- height,
- round,
- blockhash1,
- validRound1
- );
- const hash1 = hashjs
- .sha256()
- .update(proposeMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const validRound2 = new BN("5327204637322492082");
- const proposeMsg2 = generateProposeMessage(
- height,
- round,
- blockhash2,
- validRound2
- );
- const hash2 = hashjs
- .sha256()
- .update(proposeMsg2)
- .digest("hex");
- const sig2 = ecsign(Buffer.from(hash2, "hex"), darknode.privateKey);
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- const caller = accounts[1];
- const darknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
-
- // first slash should pass
- await slasher.slashDuplicatePropose(
- height,
- round,
- hexBlockhash1,
- validRound1,
- sigString1,
- hexBlockhash2,
- validRound2,
- sigString2,
- {
- from: caller
- }
- );
-
- const slashPercent = new BN(await slasher.maliciousSlashPercent());
- const slashedAmount = darknodeBond
- .div(new BN(100))
- .mul(slashPercent);
-
- const newDarknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
- newDarknodeBond.should.bignumber.equal(
- darknodeBond.sub(slashedAmount)
- );
-
- // second slash should fail
- await slasher
- .slashDuplicatePropose(
- height,
- round,
- hexBlockhash1,
- validRound1,
- sigString1,
- hexBlockhash2,
- validRound2,
- sigString2,
- {
- from: caller
- }
- )
- .should.be.rejectedWith(/DarknodeSlasher: already slashed/);
- });
-
- it("should slash duplicate prevotes for the same height and round", async () => {
- const darknode = darknodes[2];
- const height = new BN("6343893498349561232");
- const round = new BN("3652348943983436532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const proposeMsg1 = generatePrevoteMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(proposeMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const proposeMsg2 = generatePrevoteMessage(
- height,
- round,
- blockhash2
- );
- const hash2 = hashjs
- .sha256()
- .update(proposeMsg2)
- .digest("hex");
- const sig2 = ecsign(Buffer.from(hash2, "hex"), darknode.privateKey);
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- const caller = accounts[1];
- const darknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
-
- // first slash should pass
- await slasher.slashDuplicatePrevote(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2,
- {
- from: caller
- }
- );
-
- const slashPercent = new BN(await slasher.maliciousSlashPercent());
- const slashedAmount = darknodeBond
- .div(new BN(100))
- .mul(slashPercent);
-
- const newDarknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
- newDarknodeBond.should.bignumber.equal(
- darknodeBond.sub(slashedAmount)
- );
-
- // second slash should fail
- await slasher
- .slashDuplicatePrevote(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2,
- {
- from: caller
- }
- )
- .should.be.rejectedWith(/DarknodeSlasher: already slashed/);
- });
-
- it("should slash duplicate precommits for the same height and round", async () => {
- const darknode = darknodes[3];
- const height = new BN("4398348948349561232");
- const round = new BN("3348934843983436532");
- const blockhash1 = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash1 = web3.utils.asciiToHex(blockhash1);
- const proposeMsg1 = generatePrecommitMessage(
- height,
- round,
- blockhash1
- );
- const hash1 = hashjs
- .sha256()
- .update(proposeMsg1)
- .digest("hex");
- const sig1 = ecsign(Buffer.from(hash1, "hex"), darknode.privateKey);
- const sigString1 = Ox(
- `${sig1.r.toString("hex")}${sig1.s.toString(
- "hex"
- )}${sig1.v.toString(16)}`
- );
-
- const blockhash2 = "41RLyhshTwmPyAwjPM8AmReOB/q4LLdvYpDMKt1bEFI";
- const hexBlockhash2 = web3.utils.asciiToHex(blockhash2);
- const proposeMsg2 = generatePrecommitMessage(
- height,
- round,
- blockhash2
- );
- const hash2 = hashjs
- .sha256()
- .update(proposeMsg2)
- .digest("hex");
- const sig2 = ecsign(Buffer.from(hash2, "hex"), darknode.privateKey);
- const sigString2 = Ox(
- `${sig2.r.toString("hex")}${sig2.s.toString(
- "hex"
- )}${sig2.v.toString(16)}`
- );
-
- const caller = accounts[1];
- const darknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
-
- // first slash should pass
- await slasher.slashDuplicatePrecommit(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2,
- {
- from: caller
- }
- );
-
- const slashPercent = new BN(await slasher.maliciousSlashPercent());
- const slashedAmount = darknodeBond
- .div(new BN(100))
- .mul(slashPercent);
-
- const newDarknodeBond = new BN(
- await dnr.getDarknodeBond(darknode.account.address)
- );
- newDarknodeBond.should.bignumber.equal(
- darknodeBond.sub(slashedAmount)
- );
-
- // second slash should fail
- await slasher
- .slashDuplicatePrecommit(
- height,
- round,
- hexBlockhash1,
- sigString1,
- hexBlockhash2,
- sigString2,
- {
- from: caller
- }
- )
- .should.be.rejectedWith(/DarknodeSlasher: already slashed/);
- });
-
- it("should slash when a secret message is revealed", async () => {
- const darknode = darknodes[0];
- const a = new BN("3");
- const b = new BN("7");
- const c = new BN("10");
- const d = new BN(
- "81804755166950992694975918889421430561708705428859269028015361660142001064486"
- );
- const e = new BN(
- "90693014804679621771165998959262552553277008236216558633727798007697162314221"
- );
- const f = new BN(
- "65631258835468800295340604864107498262349560547191423452833833494209803247319"
- );
- const msg = generateSecretMessage(a, b, c, d, e, f);
- const hash = hashjs
- .sha256()
- .update(msg)
- .digest("hex");
- const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
- const sigString = Ox(
- `${sig.r.toString("hex")}${sig.s.toString(
- "hex"
- )}${sig.v.toString(16)}`
- );
- // first slash should succeed
- await slasher.slashSecretReveal(a, b, c, d, e, f, sigString);
- // second slash should fail
- await slasher
- .slashSecretReveal(a, b, c, d, e, f, sigString)
- .should.be.rejectedWith(/DarknodeSlasher: already slashed/);
- });
- });
-
- it("can update DarknodeRegistry", async () => {
- const darknodeRegistry = await slasher.darknodeRegistry();
- await slasher
- .updateDarknodeRegistry(NULL)
- .should.be.rejectedWith(
- /DarknodeSlasher: invalid Darknode Registry address/
- );
-
- await slasher.updateDarknodeRegistry(accounts[0]);
- await slasher.updateDarknodeRegistry(darknodeRegistry);
- });
-});
diff --git a/test/ERC20WithFees.ts b/test/ERC20WithFees.ts
deleted file mode 100644
index 6f117a6f..00000000
--- a/test/ERC20WithFees.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-import BN from "bn.js";
-
-import {
- ERC20WithFeesTestInstance,
- ReturnsFalseTokenInstance
-} from "../types/truffle-contracts";
-import "./helper/testUtils";
-
-const ERC20WithFeesTest = artifacts.require("ERC20WithFeesTest");
-const NormalToken = artifacts.require("NormalToken");
-const ReturnsFalseToken = artifacts.require("ReturnsFalseToken");
-const NonCompliantToken = artifacts.require("NonCompliantToken");
-const TokenWithFees = artifacts.require("TokenWithFees");
-
-contract("ERC20WithFees", accounts => {
- let mock: ERC20WithFeesTestInstance;
-
- before(async () => {
- mock = await ERC20WithFeesTest.new();
- });
-
- const testCases = [
- {
- contract: NormalToken,
- fees: 0,
- desc: "standard token [true for success, throws for failure]"
- },
- {
- contract: ReturnsFalseToken,
- fees: 0,
- desc: "alternate token [true for success, false for failure]"
- },
- {
- contract: NonCompliantToken,
- fees: 0,
- desc: "non compliant token [nil for success, throws for failure]"
- },
- {
- contract: TokenWithFees,
- fees: 3,
- desc: "token with fees [true for success, throws for failure]"
- }
- ];
-
- const VALUE = new BN(100000000000000);
-
- for (const testCase of testCases) {
- context(testCase.desc, async () => {
- let token: ReturnsFalseTokenInstance;
- const FEE = VALUE.mul(new BN(testCase.fees)).div(new BN(1000));
-
- before(async () => {
- token = (await testCase.contract.new()) as ReturnsFalseTokenInstance;
- });
-
- it("approve and transferFrom", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Approve and deposit
- await token.approve(mock.address, VALUE);
- await mock.deposit(token.address, VALUE);
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before.sub(new BN(VALUE))
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after.add(new BN(VALUE.sub(FEE)))
- );
- });
-
- it("transfer", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- const NEW_VALUE = VALUE.sub(FEE);
- const NEW_FEE = NEW_VALUE.mul(new BN(testCase.fees)).div(
- new BN(1000)
- );
-
- // Withdraw
- await mock.withdraw(token.address, NEW_VALUE);
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before.add(new BN(NEW_VALUE.sub(NEW_FEE)))
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after.sub(new BN(NEW_VALUE))
- );
- });
-
- it("throws for invalid transferFrom", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Approve and deposit
- await token.approve(mock.address, 0);
- await mock
- .naiveDeposit(token.address, VALUE)
- .should.be.rejectedWith(
- /SafeERC20: (ERC20 operation did not succeed)|(low-level call failed)/
- );
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after
- );
- });
-
- it("throws for invalid transferFrom (with fee)", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Approve and deposit
- await token.approve(mock.address, 0);
- await mock
- .deposit(token.address, VALUE)
- .should.be.rejectedWith(
- /SafeERC20: (ERC20 operation did not succeed)|(low-level call failed)/
- );
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after
- );
- });
-
- it("throws for invalid transfer", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Withdraw
- await mock
- .withdraw(token.address, VALUE.mul(new BN(2)))
- .should.be.rejectedWith(
- /SafeERC20: (ERC20 operation did not succeed)|(low-level call failed)/
- );
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after
- );
- });
-
- it("throws for invalid approve", async () => {
- // Transfer to the contract
- await token.transfer(mock.address, VALUE);
-
- // Subtract fees
- const NEW_VALUE = VALUE.sub(FEE);
- const NEW_FEE = NEW_VALUE.mul(new BN(testCase.fees)).div(
- new BN(1000)
- );
-
- // Get balances before transferring back
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Approve twice without resetting allowance
- await mock.approve(token.address, NEW_VALUE);
- await mock
- .approve(token.address, NEW_VALUE)
- .should.be.rejectedWith(
- /SafeERC20: approve from non-zero to non-zero allowance/
- );
-
- // Can transfer from the contract
- await token.transferFrom(
- mock.address,
- accounts[0],
- NEW_VALUE.sub(NEW_FEE)
- );
-
- // Subtract fees second time
- const NEW_NEW_VALUE = NEW_VALUE.sub(NEW_FEE);
- const NEW_NEW_FEE = NEW_NEW_VALUE.mul(
- new BN(testCase.fees)
- ).div(new BN(1000));
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before.add(new BN(NEW_NEW_VALUE.sub(NEW_NEW_FEE)))
- );
- (await token.balanceOf(mock.address)).should.bignumber.equal(
- after.sub(new BN(NEW_NEW_VALUE))
- );
- });
-
- it("throws for naive deposit if it has fees", async () => {
- // Get balances before depositing
- const before = new BN(await token.balanceOf(accounts[0]));
- const after = new BN(await token.balanceOf(mock.address));
-
- // Approve and deposit
- await token.approve(mock.address, VALUE);
- if (testCase.fees) {
- await mock
- .naiveDeposit(token.address, VALUE)
- .should.be.rejectedWith(
- /ERC20WithFeesTest: incorrect balance in deposit/
- );
- await token.approve(mock.address, 0);
- } else {
- await mock.naiveDeposit(token.address, VALUE);
-
- // Compare balances after depositing
- (await token.balanceOf(accounts[0])).should.bignumber.equal(
- before.sub(new BN(VALUE.sub(FEE)))
- );
- (
- await token.balanceOf(mock.address)
- ).should.bignumber.equal(after.add(new BN(VALUE)));
- }
- });
- });
- }
-});
diff --git a/test/Protocol.ts b/test/Protocol.ts
deleted file mode 100644
index adc06c78..00000000
--- a/test/Protocol.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import {
- DarknodeRegistryLogicV1Instance,
- ProtocolInstance
-} from "../types/truffle-contracts";
-import { NULL, waitForEpoch } from "./helper/testUtils";
-
-const DarknodeRegistryLogicV1 = artifacts.require("DarknodeRegistryLogicV1");
-const DarknodeRegistryProxy = artifacts.require("DarknodeRegistryProxy");
-const Protocol = artifacts.require("Protocol");
-
-contract("Protocol", ([owner, otherAccount]: string[]) => {
- let dnr: DarknodeRegistryLogicV1Instance;
- let protocol: ProtocolInstance;
-
- before(async () => {
- const dnrProxy = await DarknodeRegistryProxy.deployed();
- dnr = await DarknodeRegistryLogicV1.at(dnrProxy.address);
- protocol = await Protocol.at(Protocol.address);
- await waitForEpoch(dnr);
- });
-
- it("Address getters", async () => {
- (await protocol.getContract("DarknodeRegistry")).should.equal(
- dnr.address
- );
- });
-
- it("Protocol owner", async () => {
- (await protocol.owner()).should.equal(owner);
-
- await protocol
- .transferOwnership(otherAccount, { from: otherAccount })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
-
- await protocol.transferOwnership(otherAccount);
-
- (await protocol.owner()).should.equal(owner);
-
- await protocol.claimOwnership({ from: otherAccount });
-
- (await protocol.owner()).should.equal(otherAccount);
-
- await protocol.transferOwnership(owner, { from: otherAccount });
- await protocol.claimOwnership({ from: owner });
-
- (await protocol.owner()).should.equal(owner);
- });
-
- it("Update DarknodeRegistry address", async () => {
- await protocol
- .updateContract("DarknodeRegistry", NULL, { from: otherAccount })
- .should.be.rejectedWith(/Ownable: caller is not the owner/);
-
- await protocol.updateContract("DarknodeRegistry", NULL);
-
- (await protocol.getContract("DarknodeRegistry")).should.equal(NULL);
-
- await protocol.updateContract("DarknodeRegistry", dnr.address);
-
- (await protocol.getContract("DarknodeRegistry")).should.equal(
- dnr.address
- );
- });
-
- it("Proxy functions", async () => {
- // Try to initialize again
- await protocol
- .__Protocol_init(owner, { from: owner })
- .should.be.rejectedWith(
- /Contract instance has already been initialized/
- );
- });
-});
diff --git a/test/String.ts b/test/String.ts
deleted file mode 100644
index d3868c65..00000000
--- a/test/String.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import BN = require("bn.js");
-
-import { StringTestInstance } from "../types/truffle-contracts";
-import { randomBytes } from "./helper/testUtils";
-
-const StringTest = artifacts.require("StringTest");
-
-contract("String", accounts => {
- let StringInstance: StringTestInstance;
-
- before(async () => {
- StringInstance = await StringTest.new();
- });
-
- it("can add strings", async () => {
- (await StringInstance.add4("1", "2", "3", "4")).should.equal("1234");
- });
-
- it("can convert addresses to hex strings", async () => {
- (await StringInstance.fromAddress(accounts[0])).should.equal(
- accounts[0].toLowerCase()
- );
- });
-
- it("can convert bytes32 to hex strings", async () => {
- const bytes32 = randomBytes(32);
-
- (await StringInstance.fromBytes32(bytes32)).should.equal(
- bytes32.toLowerCase()
- );
- });
-
- it("can convert uint to strings", async () => {
- await testNumString("0");
- await testNumString("1");
- await testNumString("12345");
- await testNumString(
- "81804755166950992694975918889421430561708705428859269028015361660142001064486"
- );
- await testNumString(
- "90693014804679621771165998959262552553277008236216558633727798007697162314221"
- );
- await testNumString(
- "65631258835468800295340604864107498262349560547191423452833833494209803247319"
- );
- });
-
- const testNumString = async (numString: string) => {
- (await StringInstance.fromUint(new BN(numString))).should.equal(
- numString
- );
- };
-});
diff --git a/test/Validate.ts b/test/Validate.ts
deleted file mode 100644
index cb014443..00000000
--- a/test/Validate.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-import BN from "bn.js";
-import { ecsign } from "ethereumjs-util";
-import hashjs from "hash.js";
-import { Account } from "web3-eth-accounts";
-
-import { ValidateTestInstance } from "../types/truffle-contracts";
-import { Ox } from "./helper/testUtils";
-
-export interface Darknode {
- account: Account;
- privateKey: Buffer;
-}
-
-const ValidateTest = artifacts.require("ValidateTest");
-
-const numDarknodes = 2;
-
-contract("Validate", (accounts: string[]) => {
- let validateTest: ValidateTestInstance;
- const darknodes = new Array();
-
- before(async () => {
- validateTest = await ValidateTest.new();
-
- for (let i = 0; i < numDarknodes; i++) {
- const darknode = web3.eth.accounts.create();
- const privKey = Buffer.from(darknode.privateKey.slice(2), "hex");
- darknodes.push({
- account: darknode,
- privateKey: privKey
- });
- }
- });
-
- describe("when generating messages", async () => {
- it("should correctly generate secret messages", async () => {
- const a = new BN("3");
- const b = new BN("7");
- const c = new BN("10");
- const d = new BN(
- "81804755166950992694975918889421430561708705428859269028015361660142001064486"
- );
- const e = new BN(
- "90693014804679621771165998959262552553277008236216558633727798007697162314221"
- );
- const f = new BN(
- "65631258835468800295340604864107498262349560547191423452833833494209803247319"
- );
- const msg = generateSecretMessage(a, b, c, d, e, f);
- // tslint:disable-next-line:max-line-length
- msg.should.be.equal(
- "Secret(ShamirShare(3,7,S256N(10),S256PrivKey(S256N(81804755166950992694975918889421430561708705428859269028015361660142001064486),S256P(90693014804679621771165998959262552553277008236216558633727798007697162314221),S256P(65631258835468800295340604864107498262349560547191423452833833494209803247319))))"
- );
- const rawMsg = await validateTest.secretMessage(a, b, c, d, e, f);
- msg.should.be.equal(web3.utils.hexToAscii(rawMsg));
- });
-
- it("should correctly generate the propose message", async () => {
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const validRound = new BN("6345888412984379713");
- const proposeMsg = generateProposeMessage(
- height,
- round,
- blockhash,
- validRound
- );
- const rawMsg = await validateTest.proposeMessage(
- height,
- round,
- hexBlockhash,
- validRound
- );
- proposeMsg.should.be.equal(web3.utils.hexToAscii(rawMsg));
- });
-
- it("should correctly generate the prevote message", async () => {
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const prevoteMsg = generatePrevoteMessage(height, round, blockhash);
- const rawMsg = await validateTest.prevoteMessage(
- height,
- round,
- hexBlockhash
- );
- prevoteMsg.should.be.equal(web3.utils.hexToAscii(rawMsg));
- });
-
- it("should correctly generate the precommit message", async () => {
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const precommitMsg = generatePrecommitMessage(
- height,
- round,
- blockhash
- );
- const rawMsg = await validateTest.precommitMessage(
- height,
- round,
- hexBlockhash
- );
- precommitMsg.should.be.equal(web3.utils.hexToAscii(rawMsg));
- });
- });
-
- describe("when recovering signatures", async () => {
- it("can recover the signer of a propose message", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const validRound = new BN("6345888412984379713");
- const proposeMsg = generateProposeMessage(
- height,
- round,
- blockhash,
- validRound
- );
- const hash = hashjs
- .sha256()
- .update(proposeMsg)
- .digest("hex");
- const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
- const sigString = Ox(
- `${sig.r.toString("hex")}${sig.s.toString(
- "hex"
- )}${sig.v.toString(16)}`
- );
- const signer = await validateTest.recoverPropose(
- height,
- round,
- hexBlockhash,
- validRound,
- sigString
- );
- signer.should.equal(darknode.account.address);
- });
-
- it("can recover the signer of a prevote message", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const proposeMsg = generatePrevoteMessage(height, round, blockhash);
- const hash = hashjs
- .sha256()
- .update(proposeMsg)
- .digest("hex");
- const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
- const sigString = Ox(
- `${sig.r.toString("hex")}${sig.s.toString(
- "hex"
- )}${sig.v.toString(16)}`
- );
- const signer = await validateTest.recoverPrevote(
- height,
- round,
- hexBlockhash,
- sigString
- );
- signer.should.equal(darknode.account.address);
- });
-
- it("can recover the signer of a precommit message", async () => {
- const darknode = darknodes[0];
- const height = new BN("6349374925919561232");
- const round = new BN("3652381888914236532");
- const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
- const hexBlockhash = web3.utils.asciiToHex(blockhash);
- const proposeMsg = generatePrecommitMessage(
- height,
- round,
- blockhash
- );
- const hash = hashjs
- .sha256()
- .update(proposeMsg)
- .digest("hex");
- const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
- const sigString = Ox(
- `${sig.r.toString("hex")}${sig.s.toString(
- "hex"
- )}${sig.v.toString(16)}`
- );
- const signer = await validateTest.recoverPrecommit(
- height,
- round,
- hexBlockhash,
- sigString
- );
- signer.should.equal(darknode.account.address);
- });
-
- it("can recover the signer of a secret message", async () => {
- const darknode = darknodes[0];
- const a = new BN("3");
- const b = new BN("7");
- const c = new BN("10");
- const d = new BN(
- "81804755166950992694975918889421430561708705428859269028015361660142001064486"
- );
- const e = new BN(
- "90693014804679621771165998959262552553277008236216558633727798007697162314221"
- );
- const f = new BN(
- "65631258835468800295340604864107498262349560547191423452833833494209803247319"
- );
- const msg = generateSecretMessage(a, b, c, d, e, f);
- const hash = hashjs
- .sha256()
- .update(msg)
- .digest("hex");
- const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
- const sigString = Ox(
- `${sig.r.toString("hex")}${sig.s.toString(
- "hex"
- )}${sig.v.toString(16)}`
- );
- const signer = await validateTest.recoverSecret(
- a,
- b,
- c,
- d,
- e,
- f,
- sigString
- );
- signer.should.equal(darknode.account.address);
- });
- });
-});
-
-export const generateProposeMessage = (
- height: BN,
- round: BN,
- blockHash: string,
- validRound: BN
-): string => {
- // tslint:disable-next-line:max-line-length
- return `Propose(Height=${height.toString()},Round=${round.toString()},BlockHash=${blockHash},ValidRound=${validRound.toString()})`;
-};
-
-export const generatePrevoteMessage = (
- height: BN,
- round: BN,
- blockHash: string
-): string => {
- return `Prevote(Height=${height.toString()},Round=${round.toString()},BlockHash=${blockHash})`;
-};
-
-export const generatePrecommitMessage = (
- height: BN,
- round: BN,
- blockHash: string
-): string => {
- return `Precommit(Height=${height.toString()},Round=${round.toString()},BlockHash=${blockHash})`;
-};
-
-export const generateSecretMessage = (
- a: BN,
- b: BN,
- c: BN,
- d: BN,
- e: BN,
- f: BN
-): string => {
- // tslint:disable-next-line:max-line-length
- return `Secret(ShamirShare(${a.toString()},${b.toString()},S256N(${c.toString()}),S256PrivKey(S256N(${d.toString()}),S256P(${e.toString()}),S256P(${f.toString()}))))`;
-};
diff --git a/test/helper/testUtils.ts b/test/helper/testUtils.ts
index 11562f81..47b0a104 100644
--- a/test/helper/testUtils.ts
+++ b/test/helper/testUtils.ts
@@ -1,17 +1,21 @@
-import * as chai from "chai";
+// Import chai log helper
+import "./logs";
+
import * as crypto from "crypto";
import BigNumber from "bignumber.js";
import BN from "bn.js";
+import * as chai from "chai";
import chaiAsPromised from "chai-as-promised";
import chaiBigNumber from "chai-bignumber";
import { ECDSASignature } from "ethereumjs-util";
import { TransactionReceipt } from "web3-core";
import { keccak256, toChecksumAddress } from "web3-utils";
-import { DarknodeRegistryLogicV1Instance } from "../../types/truffle-contracts";
-// Import chai log helper
-import "./logs";
+import {
+ DarknodeRegistryLogicV1Instance,
+ DarknodeRegistryLogicV2Instance,
+} from "../../types/truffle-contracts";
const ERC20 = artifacts.require("PaymentToken");
@@ -120,7 +124,9 @@ export const increaseTime = async (seconds: number) => {
} while (currentTimestamp < target);
};
-export async function waitForEpoch(dnr: DarknodeRegistryLogicV1Instance) {
+export async function waitForEpoch(
+ dnr: DarknodeRegistryLogicV1Instance | DarknodeRegistryLogicV2Instance
+) {
// const timeout = MINIMUM_EPOCH_INTERVAL_SECONDS;
const timeout = new BN(
(await dnr.minimumEpochInterval()).toString()
@@ -249,3 +255,27 @@ export const toBN = <
};
export const range = (n: number) => Array.from(new Array(n)).map((_, i) => i);
+
+export const signRecoverMessage = async (
+ owner: string,
+ recipient: string,
+ darknodeID: string
+) => {
+ // Recover
+ const signature = Buffer.from(
+ (
+ await web3.eth.sign(
+ "0x" +
+ Buffer.concat([
+ Buffer.from("DarknodeRegistry.recover"),
+ Buffer.from(darknodeID.slice(2), "hex"),
+ Buffer.from(recipient.slice(2), "hex"),
+ ]).toString("hex"),
+ owner
+ )
+ ).slice(2),
+ "hex"
+ );
+ signature[64] = (signature[64] % 27) + 27;
+ return "0x" + signature.toString("hex");
+};