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