diff --git a/.solcover.js b/.solcover.js new file mode 100644 index 0000000..03db1f6 --- /dev/null +++ b/.solcover.js @@ -0,0 +1,22 @@ +module.exports = { + skipFiles: [ + 'Migrations.sol', + 'test/BlockRewardMock.sol', + 'test/ConsensusMock.sol', + 'test/EternalStorageProxyMock.sol', + 'test/ProxyStorageMock.sol', + 'test/VotingMock.sol', + ], + // need for dependencies + copyNodeModules: true, + copyPackages: [ + 'openzeppelin-solidity' + ], + dir: '.', + providerOptions: { + total_accounts: 110, + default_balance_ether: 100000000, + gasPrice: '0x1' + }, + norpc: false +}; diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..217db7a --- /dev/null +++ b/.solhint.json @@ -0,0 +1,22 @@ +{ + "extends": "solhint:default", + "rules": { + "avoid-throw": "off", + "avoid-suicide": "error", + "avoid-sha3": "warn", + "indent": ["warn", 4], + "compiler-fixed": "off", + "not-rely-on-time": "off", + "quotes": ["error", "double"], + "no-empty-blocks": "off", + "no-complex-fallback": "off", + "two-lines-top-level-separator": "off", + "code-complexity": 8, + "avoid-call-value": "off", + "no-simple-event-func-name": "off", + "avoid-low-level-calls": "off", + "no-inline-assembly": "off", + "max-line-length": 170, + "bracket-align": "off" + } +} diff --git a/.soliumrc.json b/.soliumrc.json new file mode 100644 index 0000000..3f27141 --- /dev/null +++ b/.soliumrc.json @@ -0,0 +1,44 @@ +{ + "extends": "solium:recommended", + "plugins": [ + "security" + ], + "rules": { + "quotes": [ + "error", + "double" + ], + "indentation": [ + "error", + 4 + ], + "max-len": ["error", 79], + "lbrace": "off", + "linebreak-style": ["error", "unix"], + "no-constant": ["error"], + "no-empty-blocks": "off", + "uppercase": "off", + "visibility-first": "error", + "security/enforce-explicit-visibility": ["error"], + "security/no-block-members": ["warning"], + "security/no-inline-assembly": ["warning"], + "blank-lines": "off", + "imports-on-top": "error", + "array-declarations": "warning", + "operator-whitespace": "warning", + "conditionals-whitespace": "warning", + "semicolon-whitespace": "warning", + "function-whitespace": "warning", + "mixedcase": "warning", + "no-unused-vars": "warning", + "pragma-on-top": "error", + "function-order": "warning", + "emit": "error", + "value-in-payable": "error", + "error-reason": "warning", + "no-experimental": "warning", + "deprecated-suicide": "error", + "whitespace": "warning", + "arg-overflow": "error" + } +} diff --git a/contracts/BlockReward.sol b/contracts/BlockReward.sol index 8191e88..05c22ed 100644 --- a/contracts/BlockReward.sol +++ b/contracts/BlockReward.sol @@ -85,7 +85,7 @@ contract BlockReward is EternalStorage, BlockRewardBase { require(benefactors.length == 1); require(kind[0] == 0); - uint256 blockRewardAmount = getBlockRewardAmount(); + uint256 blockRewardAmount = getBlockRewardAmountPerValidator(benefactors[0]); (address[] memory _delegators, uint256[] memory _rewards) = IConsensus(ProxyStorage(getProxyStorage()).getConsensus()).getDelegatorsForRewardDistribution(benefactors[0], blockRewardAmount); @@ -183,6 +183,20 @@ contract BlockReward is EternalStorage, BlockRewardBase { return uintStorage[BLOCK_REWARD_AMOUNT]; } + function getBlockRewardAmountPerValidator(address _validator) public view returns(uint256) { + IConsensus consensus = IConsensus(ProxyStorage(getProxyStorage()).getConsensus()); + uint256 stakeAmount = consensus.stakeAmount(_validator); + uint256 totalStakeAmount = consensus.totalStakeAmount(); + uint256 currentValidatorsLength = consensus.currentValidatorsLength(); + // this may arise in peculiar cases when the consensus totalStakeAmount wasn't calculated yet + // for example at the first blocks after the contract was deployed + if (totalStakeAmount == 0) { + return getBlockRewardAmount(); + } + return getBlockRewardAmount().mul(stakeAmount).mul(currentValidatorsLength).div(totalStakeAmount); + } + + function getProxyStorage() public view returns(address) { return addressStorage[PROXY_STORAGE]; } diff --git a/contracts/Consensus.sol b/contracts/Consensus.sol index afcf1a4..0addc5c 100644 --- a/contracts/Consensus.sol +++ b/contracts/Consensus.sol @@ -71,17 +71,7 @@ contract Consensus is ConsensusUtils { * @param _amount the amount msg.sender wishes to withdraw from the contract */ function withdraw(uint256 _amount) external { - require(_amount > 0); - require(_amount <= stakeAmount(msg.sender)); - require(_amount <= delegatedAmount(msg.sender, msg.sender)); - - _delegatedAmountSub(msg.sender, msg.sender, _amount); - _stakeAmountSub(msg.sender, _amount); - if (stakeAmount(msg.sender) < getMinStake()) { - _pendingValidatorsRemove(msg.sender); - } - - msg.sender.transfer(_amount); + _withdraw(msg.sender, _amount, msg.sender); } /** @@ -90,40 +80,17 @@ contract Consensus is ConsensusUtils { * @param _amount the amount msg.sender wishes to withdraw from the contract */ function withdraw(address _validator, uint256 _amount) external { - require(_validator != address(0)); - require(_amount > 0); - require(_amount <= stakeAmount(_validator)); - require(_amount <= delegatedAmount(msg.sender, _validator)); - - _delegatedAmountSub(msg.sender, _validator, _amount); - _stakeAmountSub(_validator, _amount); - if (stakeAmount(_validator) < getMinStake()) { - _pendingValidatorsRemove(_validator); - } - - msg.sender.transfer(_amount); + _withdraw(msg.sender, _amount, _validator); } /** * @dev Function to be called by the block reward contract each block to handle cycles and snapshots logic */ function cycle() external onlyBlockReward { - if (_shouldTakeSnapshot()) { - uint256 snapshotId = getNextSnapshotId(); - if (snapshotId == getSnapshotsPerCycle().sub(1)) { - _setNextSnapshotId(0); - } else { - _setNextSnapshotId(snapshotId.add(1)); - } - _setSnapshot(snapshotId, pendingValidators()); - _setLastSnapshotTakenAtBlock(block.number); - delete snapshotId; - } if (_hasCycleEnded()) { IVoting(ProxyStorage(getProxyStorage()).getVoting()).onCycleEnd(currentValidators()); _setCurrentCycle(); - uint256 randomSnapshotId = _getRandom(0, getSnapshotsPerCycle() - 1); - address[] memory newSet = getSnapshotAddresses(randomSnapshotId); + address[] memory newSet = pendingValidators(); if (newSet.length > 0) { _setNewValidatorSet(newSet); } @@ -132,7 +99,6 @@ contract Consensus is ConsensusUtils { _setShouldEmitInitiateChange(true); emit ShouldEmitInitiateChange(); } - delete randomSnapshotId; IBlockReward(ProxyStorage(getProxyStorage()).getBlockReward()).onCycleEnd(); } } diff --git a/contracts/ConsensusUtils.sol b/contracts/ConsensusUtils.sol index 90f73be..e87e258 100644 --- a/contracts/ConsensusUtils.sol +++ b/contracts/ConsensusUtils.sol @@ -16,8 +16,9 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { uint256 public constant DECIMALS = 10 ** 18; uint256 public constant MAX_VALIDATORS = 100; uint256 public constant MIN_STAKE = 1e23; // 100,000 - uint256 public constant CYCLE_DURATION_BLOCKS = 120960; // 7 days [7*24*60*60/5] - uint256 public constant SNAPSHOTS_PER_CYCLE = 10; // snapshot each 1008 minutes [120960/10/60*5] + uint256 public constant MAX_STAKE = 5e24; // 5,000,000 + uint256 public constant CYCLE_DURATION_BLOCKS = 34560; // 48 hours [48*60*60/5] + uint256 public constant SNAPSHOTS_PER_CYCLE = 0; // snapshot each 288 minutes [34560/10/60*5] uint256 public constant DEFAULT_VALIDATOR_FEE = 1e17; // 10% /** @@ -84,22 +85,61 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { bytes32 internal constant WAS_PROXY_STORAGE_SET = keccak256(abi.encodePacked("wasProxyStorageSet")); bytes32 internal constant NEW_VALIDATOR_SET = keccak256(abi.encodePacked("newValidatorSet")); bytes32 internal constant SHOULD_EMIT_INITIATE_CHANGE = keccak256(abi.encodePacked("shouldEmitInitiateChange")); + bytes32 internal constant TOTAL_STAKE_AMOUNT = keccak256(abi.encodePacked("totalStakeAmount")); function _delegate(address _staker, uint256 _amount, address _validator) internal { require(_staker != address(0)); require(_amount != 0); require(_validator != address(0)); - // overstaking should not be possible - require (stakeAmount(_validator) < getMinStake()); - require (stakeAmount(_validator).add(_amount) <= getMinStake()); - _delegatedAmountAdd(_staker, _validator, _amount); _stakeAmountAdd(_validator, _amount); + // stake amount of the validator isn't greater than the max stake + require(stakeAmount(_validator) <= getMaxStake()); + + // the validator must stake himselft the minimum stake if (stakeAmount(_validator) >= getMinStake() && !isPendingValidator(_validator)) { _pendingValidatorsAdd(_validator); } + + // if _validator is one of the current validators + if (isValidator(_validator)) { + // the total stake needs to be adjusted for the block reward formula + _totalStakeAmountAdd(_amount); + } + } + + function _withdraw(address _staker, uint256 _amount, address _validator) internal { + require(_validator != address(0)); + require(_amount > 0); + require(_amount <= stakeAmount(_validator)); + require(_amount <= delegatedAmount(_staker, _validator)); + + bool _isValidator = isValidator(_validator); + + // if new stake amount is lesser than minStake and the validator is one of the current validators + if (stakeAmount(_validator).sub(_amount) < getMinStake() && _isValidator) { + // do not withdaw the amount until the validator is in current set + _pendingValidatorsRemove(_validator); + return; + } + + + _delegatedAmountSub(_staker, _validator, _amount); + _stakeAmountSub(_validator, _amount); + + // if _validator is one of the current validators + if (_isValidator) { + // the total stake needs to be adjusted for the block reward formula + _totalStakeAmountSub(_amount); + } + + // if validator is needed to be removed from pending, but not current + if (stakeAmount(_validator) < getMinStake()) { + _pendingValidatorsRemove(_validator); + } + _staker.transfer(_amount); } function _setSystemAddress(address _newAddress) internal { @@ -139,10 +179,17 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { return MIN_STAKE; } + /** + * returns maximum stake (wei) for a validator + */ + function getMaxStake() public pure returns(uint256) { + return MAX_STAKE; + } + /** * returns number of blocks per cycle (block time is 5 seconds) */ - function getCycleDurationBlocks() public pure returns(uint256) { + function getCycleDurationBlocks() public view returns(uint256) { return CYCLE_DURATION_BLOCKS; } @@ -236,6 +283,12 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { } function _setCurrentValidators(address[] _currentValidators) internal { + uint256 totalStake = 0; + for (uint i = 0; i < _currentValidators.length; i++) { + uint256 stakedAmount = stakeAmount(_currentValidators[i]); + totalStake = totalStake + stakedAmount; + } + _setTotalStakeAmount(totalStake); addressArrayStorage[CURRENT_VALIDATORS] = _currentValidators; } @@ -286,6 +339,7 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { } delete addressArrayStorage[PENDING_VALIDATORS][lastIndex]; addressArrayStorage[PENDING_VALIDATORS].length--; + // if the validator in on of the current validators } } @@ -293,6 +347,10 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { return uintStorage[keccak256(abi.encodePacked("stakeAmount", _address))]; } + function totalStakeAmount() public view returns(uint256) { + return uintStorage[TOTAL_STAKE_AMOUNT]; + } + function _stakeAmountAdd(address _address, uint256 _amount) internal { uintStorage[keccak256(abi.encodePacked("stakeAmount", _address))] = uintStorage[keccak256(abi.encodePacked("stakeAmount", _address))].add(_amount); } @@ -393,20 +451,24 @@ contract ConsensusUtils is EternalStorage, ValidatorSet { addressArrayStorage[NEW_VALIDATOR_SET] = _newSet; } - function shouldEmitInitiateChange() public view returns(bool) { - return boolStorage[SHOULD_EMIT_INITIATE_CHANGE]; + function _setTotalStakeAmount(uint256 _totalStake) internal { + uintStorage[TOTAL_STAKE_AMOUNT] = _totalStake; } - function _setShouldEmitInitiateChange(bool _status) internal { - boolStorage[SHOULD_EMIT_INITIATE_CHANGE] = _status; + function _totalStakeAmountAdd(uint256 _stakeAmount) internal { + uintStorage[TOTAL_STAKE_AMOUNT] = uintStorage[TOTAL_STAKE_AMOUNT].add(_stakeAmount); + } + + function _totalStakeAmountSub(uint256 _stakeAmount) internal { + uintStorage[TOTAL_STAKE_AMOUNT] = uintStorage[TOTAL_STAKE_AMOUNT].sub(_stakeAmount); } - function _getBlocksToSnapshot() internal pure returns(uint256) { - return getCycleDurationBlocks().div(getSnapshotsPerCycle()); + function shouldEmitInitiateChange() public view returns(bool) { + return boolStorage[SHOULD_EMIT_INITIATE_CHANGE]; } - function _shouldTakeSnapshot() internal view returns(bool) { - return (block.number - getLastSnapshotTakenAtBlock() >= _getBlocksToSnapshot()); + function _setShouldEmitInitiateChange(bool _status) internal { + boolStorage[SHOULD_EMIT_INITIATE_CHANGE] = _status; } function _hasCycleEnded() internal view returns(bool) { diff --git a/contracts/interfaces/IConsensus.sol b/contracts/interfaces/IConsensus.sol index 44653db..6428da5 100644 --- a/contracts/interfaces/IConsensus.sol +++ b/contracts/interfaces/IConsensus.sol @@ -9,4 +9,6 @@ interface IConsensus { function isValidator(address _address) external view returns(bool); function getDelegatorsForRewardDistribution(address _validator, uint256 _rewardAmount) external view returns(address[], uint256[]); function isFinalized() external view returns(bool); + function stakeAmount(address _address) external view returns(uint256); + function totalStakeAmount() external view returns(uint256); } diff --git a/test/contracts/BlockRewardMock.sol b/contracts/test/BlockRewardMock.sol similarity index 78% rename from test/contracts/BlockRewardMock.sol rename to contracts/test/BlockRewardMock.sol index 4032fbe..2be02a6 100644 --- a/test/contracts/BlockRewardMock.sol +++ b/contracts/test/BlockRewardMock.sol @@ -1,6 +1,6 @@ pragma solidity ^0.4.24; -import "../../contracts/BlockReward.sol"; +import "../BlockReward.sol"; contract BlockRewardMock is BlockReward { function setSystemAddressMock(address _newAddress) public onlyOwner { @@ -18,4 +18,8 @@ contract BlockRewardMock is BlockReward { function setShouldEmitRewardedOnCycleMock(bool _status) public { boolStorage[SHOULD_EMIT_REWARDED_ON_CYCLE] = _status; } + + function cycleMock() public { + IConsensus(ProxyStorage(getProxyStorage()).getConsensus()).cycle(); + } } diff --git a/contracts/test/ConsensusMock.sol b/contracts/test/ConsensusMock.sol new file mode 100644 index 0000000..fea7d37 --- /dev/null +++ b/contracts/test/ConsensusMock.sol @@ -0,0 +1,88 @@ +pragma solidity ^0.4.24; + +import "../Consensus.sol"; + +contract ConsensusMock is Consensus { + uint256 currentValidatorsLengthMock = 0; + uint256 private blockTest = 120; + + function setSystemAddressMock(address _newAddress) public onlyOwner { + addressStorage[SYSTEM_ADDRESS] = _newAddress; + } + + function getSystemAddress() public view returns(address) { + return addressStorage[SYSTEM_ADDRESS]; + } + + function hasCycleEnded() public view returns(bool) { + return _hasCycleEnded(); + } + + // function shouldTakeSnapshot() public view returns(bool) { + // return _shouldTakeSnapshot(); + // } + + function getRandom(uint256 _from, uint256 _to) public view returns(uint256) { + return _getRandom(_from, _to); + } + + // function getBlocksToSnapshot() public pure returns(uint256) { + // return _getBlocksToSnapshot(); + // } + + function setNewValidatorSetMock(address[] _newSet) public { + addressArrayStorage[NEW_VALIDATOR_SET] = _newSet; + } + + function setFinalizedMock(bool _status) public { + boolStorage[IS_FINALIZED] = _status; + } + + function setShouldEmitInitiateChangeMock(bool _status) public { + boolStorage[SHOULD_EMIT_INITIATE_CHANGE] = _status; + } + + function getMinStake() public pure returns(uint256) { + return 1e22; + } + + function getMaxStake() public pure returns(uint256) { + return 5e22; + } + + function getCycleDurationBlocks() public view returns(uint256) { + return blockTest == 0 ? 120 : blockTest; + } + + function setCycleDurationBlocks(uint256 _block) public { + blockTest = _block; + } + + function setCurrentCycleEndBlock(uint256 _value) public { + uintStorage[CURRENT_CYCLE_END_BLOCK] = _value; + } + + function getSnapshotsPerCycle() public pure returns(uint256) { + return 10; + } + + function setCurrentValidatorsLengthMock(uint256 _currentValidatorsLengthMock) external { + currentValidatorsLengthMock = _currentValidatorsLengthMock; + } + + function currentValidatorsLength() public view returns(uint256) { + if (currentValidatorsLengthMock != 0) { + return currentValidatorsLengthMock; + } + return super.currentValidatorsLength(); + } + + function setValidatorFeeMock(uint256 _amount) external { + require (_amount <= 1 * DECIMALS); + _setValidatorFee(msg.sender, _amount); + } + + function setTotalStakeAmountMock(uint256 _totalStake) public { + _setTotalStakeAmount(_totalStake); + } +} diff --git a/test/contracts/EternalStorageProxyMock.sol b/contracts/test/EternalStorageProxyMock.sol similarity index 84% rename from test/contracts/EternalStorageProxyMock.sol rename to contracts/test/EternalStorageProxyMock.sol index 72730d4..ebcf07c 100644 --- a/test/contracts/EternalStorageProxyMock.sol +++ b/contracts/test/EternalStorageProxyMock.sol @@ -1,6 +1,6 @@ pragma solidity ^0.4.24; -import '../../contracts/eternal-storage/EternalStorageProxy.sol'; +import '../eternal-storage/EternalStorageProxy.sol'; contract EternalStorageProxyMock is EternalStorageProxy { constructor(address _proxyStorage, address _implementation) EternalStorageProxy(_proxyStorage, _implementation) public {} diff --git a/test/contracts/ProxyStorageMock.sol b/contracts/test/ProxyStorageMock.sol similarity index 93% rename from test/contracts/ProxyStorageMock.sol rename to contracts/test/ProxyStorageMock.sol index 92e4a0b..7f92f04 100644 --- a/test/contracts/ProxyStorageMock.sol +++ b/contracts/test/ProxyStorageMock.sol @@ -1,6 +1,6 @@ pragma solidity ^0.4.24; -import "../../contracts/ProxyStorage.sol"; +import "../ProxyStorage.sol"; contract ProxyStorageMock is ProxyStorage { function setBlockRewardMock(address _newAddress) public { diff --git a/contracts/test/VotingMock.sol b/contracts/test/VotingMock.sol new file mode 100644 index 0000000..fd545bd --- /dev/null +++ b/contracts/test/VotingMock.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.24; + +import "../Voting.sol"; + +contract VotingMock is Voting { + + function setNextBallotIdMock(uint256 _id) public { + uintStorage[NEXT_BALLOT_ID] = _id; + } + + function setAcceptedMock(uint256 _id, uint256 _value) public { + _setAccepted(_id, _value); + } + + function setBalotStartBlockMock(uint256 _balotId, uint256 block) public { + uintStorage[keccak256(abi.encodePacked("votingState", _balotId, "startBlock"))] = block; + } + function setBalotEndBlockMock(uint256 _balotId, uint256 block) public { + uintStorage[keccak256(abi.encodePacked("votingState", _balotId, "endBlock"))] = block; + } +} diff --git a/migrations/2_deploy_contract.js b/migrations/2_deploy_contract.js index 7842e40..33761bf 100644 --- a/migrations/2_deploy_contract.js +++ b/migrations/2_deploy_contract.js @@ -78,6 +78,8 @@ module.exports = function(deployer, network, accounts) { await proxyStorage.initializeAddresses(blockReward.address, voting.address) debug(`proxyStorage.initializeAddresses: ${blockReward.address}, ${voting.address}`) + // TODO: + // stake to consensus on behalf of the initial validator if (!!SAVE_TO_FILE === true) { const contracts = { "BlockReward": blockReward.address, diff --git a/package.json b/package.json index 8bfddd1..090df67 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,29 @@ "main": "", "scripts": { "test": "scripts/test.sh", + "coverage": "node_modules/.bin/truffle run coverage", "compile": "node_modules/.bin/truffle compile", "flatten": "scripts/flatten.sh", "abi": "scripts/abi.sh", "deploy:fuse": "node_modules/.bin/truffle migrate --reset --network fuse", "deploy:local": "node_modules/.bin/truffle migrate --reset --network local", - "app": "node app/index.js" + "app": "node app/index.js", + "solhint": "node ./node_modules/solhint/solhint.js contracts/*.sol", + "solium": "solium -d contracts/" }, "repository": { "type": "git", "url": "git+https://github.com/fuseio/fuse-network.git" }, + "standard": { + "env": { + "mocha": true, + "truffle/globals": true + }, + "plugins": [ + "truffle" + ] + }, "author": "Lior Rabin", "license": "MIT", "dependencies": { @@ -33,6 +45,10 @@ "node-jq": "^1.9.0", "solc": "0.4.24", "truffle": "^5.0.24", - "truffle-flattener": "^1.3.0" + "truffle-flattener": "^1.3.0", + "solidity-coverage": "^0.7.11", + "solhint": "^2.3.0", + "solium": "^1.2.5", + "bn": "^1.0.5" } } diff --git a/test/blockReward.test.js b/test/blockReward.test.js index f31e7c3..f71a1a3 100644 --- a/test/blockReward.test.js +++ b/test/blockReward.test.js @@ -1,10 +1,15 @@ +/* eslint-disable prefer-const */ +/* eslint-disable object-curly-spacing */ const Consensus = artifacts.require('ConsensusMock.sol') const ProxyStorage = artifacts.require('ProxyStorageMock.sol') const EternalStorageProxy = artifacts.require('EternalStorageProxyMock.sol') const BlockReward = artifacts.require('BlockRewardMock.sol') -const Voting = artifacts.require('Voting.sol') -const {ERROR_MSG, ZERO_ADDRESS, RANDOM_ADDRESS} = require('./helpers') +const Voting = artifacts.require('VotingMock.sol') +const { ERROR_MSG, ZERO_ADDRESS, RANDOM_ADDRESS } = require('./helpers') +const utils = require("./utils"); +const { ZERO, ONE, TWO, THREE, FOUR, TEN } = require('./helpers') const {toBN, toWei, toChecksumAddress} = web3.utils +const CONTRACT_TYPES = { INVALID: 0, CONSENSUS: 1, BLOCK_REWARD: 2, PROXY_STORAGE: 3, VOTING: 4 } const INITIAL_SUPPLY = toWei(toBN(300000000000000000 || 0), 'gwei') const BLOCKS_PER_YEAR = 100 @@ -12,11 +17,11 @@ const YEARLY_INFLATION_PERCENTAGE = 5 const SYSTEM_ADDRESS = '0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE' contract('BlockReward', async (accounts) => { - let blockRewardImpl, proxy, blockReward + let blockRewardImpl, proxy, blockReward, consensusImpl, consensus, proxyStorageImpl, proxyStorage let owner = accounts[0] let nonOwner = accounts[1] let mockSystemAddress = accounts[2] - let voting = accounts[3] + let voting beforeEach(async () => { // Consensus @@ -38,9 +43,9 @@ contract('BlockReward', async (accounts) => { blockReward = await BlockReward.at(proxy.address) // Voting - let votingImpl = await Voting.new() + const votingImpl = await Voting.new() proxy = await EternalStorageProxy.new(proxyStorage.address, votingImpl.address) - let voting = await Voting.at(proxy.address) + voting = await Voting.at(proxy.address) // Initialize ProxyStorage await proxyStorage.initializeAddresses( @@ -69,12 +74,133 @@ contract('BlockReward', async (accounts) => { }) describe('reward', async () => { + let minStakeAmount, doubleMinStakeAmount beforeEach(async () => { await blockReward.initialize(INITIAL_SUPPLY) + minStakeAmount = await consensus.getMinStake() + doubleMinStakeAmount = minStakeAmount.mul(TWO) }) + + describe('#getBlockRewardAmountPerValidator', () => { + let blockRewardAmount + let validator, secondValidator + beforeEach(async () => { + blockRewardAmount = await blockReward.getBlockRewardAmount() + validator = accounts[1] + secondValidator = accounts[2] + }) + + it('block reward with one validator', async () => { + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + // mocking total supply + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(TEN)) + + const l = await consensus.currentValidatorsLength() + '1'.should.be.equal(l.toString(10)) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + const expectedReward = blockRewardAmount.div(TEN) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward of one validator staking 100% of the total stake', async () => { + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + await consensus.setTotalStakeAmountMock(minStakeAmount) + + const l = await consensus.currentValidatorsLength() + '1'.should.be.equal(l.toString(10)) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + const expectedReward = blockRewardAmount + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward of 1 validator of 2, staking 10% of the total stake', async () => { + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(TEN)) + await consensus.setCurrentValidatorsLengthMock(TWO) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + + // expected reward calculation + const expectedReward = blockRewardAmount.div(TEN).mul(TWO) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward of 1 validator of 2, staking 50% of the total stake', async () => { + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(TEN)) + await consensus.setCurrentValidatorsLengthMock(TWO) + + const l = await consensus.currentValidatorsLength() + '2'.should.be.equal(l.toString(10)) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + // expected reward calculation + const expectedReward = blockRewardAmount.div(TEN).mul(TWO) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward does not change if the propotion stays the same', async () => { + const validator = accounts[0] + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(TEN)) + await consensus.setCurrentValidatorsLengthMock(TWO) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + + // validator stake is 5 * minStakeAmount now + await consensus.sendTransaction({ from: validator, value: minStakeAmount.mul(toBN(4)) }).should.be.fulfilled + // total stake is 10 * minStakeAmount now + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(TEN)) + + // expected reward calculation + const expectedReward = blockRewardAmount.div(TEN).mul(TWO) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward for two validators', async () => { + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + await consensus.sendTransaction({ from: secondValidator, value: minStakeAmount.mul(THREE) }).should.be.fulfilled + + await consensus.setTotalStakeAmountMock(minStakeAmount.mul(FOUR)) + await consensus.setCurrentValidatorsLengthMock(TWO) + + let blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + let expectedReward = blockRewardAmount.div(FOUR).mul(TWO) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + + blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(secondValidator) + expectedReward = blockRewardAmount.mul(THREE).div(FOUR).mul(TWO) + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + it('block reward without the total stake', async () => { + const minStakeAmount = await consensus.getMinStake() + + const validator = accounts[1] + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled + // await consensus.setTotalStakeAmountMock(minStakeAmount) + + const l = await consensus.currentValidatorsLength() + '1'.should.be.equal(l.toString(10)) + const k = await consensus.requiredSignatures() + '1'.should.be.equal(k.toString(10)) + + const blockRewardAmountOfV = await blockReward.getBlockRewardAmountPerValidator(validator) + const expectedReward = blockRewardAmount + expectedReward.should.be.bignumber.equal(blockRewardAmountOfV) + }) + + }) + it('can only be called by system address', async () => { await blockReward.reward([accounts[3]], [0]).should.be.rejectedWith(ERROR_MSG) await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.sendTransaction({from: owner, value: minStakeAmount}).should.be.fulfilled await blockReward.reward([accounts[3]], [0], {from: mockSystemAddress}).should.be.fulfilled }) it('should revert if input array contains more than one item', async () => { @@ -90,13 +216,17 @@ contract('BlockReward', async (accounts) => { await blockReward.reward([accounts[3]], [1], {from: mockSystemAddress}).should.be.rejectedWith(ERROR_MSG) }) it('should give reward to validator and total supply should be updated', async () => { + const validator = accounts[3] await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.setTotalStakeAmountMock(minStakeAmount) + await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled + let initialSupply = await blockReward.getTotalSupply() - let blockRewardAmount = await blockReward.getBlockRewardAmount() - let {logs} = await blockReward.reward([accounts[3]], [0], {from: mockSystemAddress}).should.be.fulfilled + let blockRewardAmount = await blockReward.getBlockRewardAmountPerValidator(validator) + let {logs} = await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled logs.length.should.be.equal(1) logs[0].event.should.be.equal('Rewarded') - logs[0].args['receivers'].should.deep.equal([accounts[3]]) + logs[0].args['receivers'].should.deep.equal([validator]) logs[0].args['rewards'][0].should.be.bignumber.equal(blockRewardAmount) let expectedSupply = initialSupply.add(blockRewardAmount) expectedSupply.should.be.bignumber.equal(await blockReward.getTotalSupply()) @@ -110,7 +240,7 @@ contract('BlockReward', async (accounts) => { let stakeAmountValue = minStakeAmount.div(decimals).toNumber() - delegateAmountValue * delegatorsCount let stakeAmount = toWei(toBN(stakeAmountValue), 'ether') let fee = 5 - let validator = accounts[1] + let validator = accounts[0] await consensus.sendTransaction({from: validator, value: stakeAmount}).should.be.fulfilled for (let i = 2; i < accounts.length; i++) { await consensus.delegate(validator, {from: accounts[i], value: delegateAmount}).should.be.fulfilled @@ -119,7 +249,7 @@ contract('BlockReward', async (accounts) => { let validatorFee = await consensus.validatorFee(validator) await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) let initialSupply = await blockReward.getTotalSupply() - let blockRewardAmount = await blockReward.getBlockRewardAmount() + let blockRewardAmount = await blockReward.getBlockRewardAmountPerValidator(validator) let {logs} = await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled logs.length.should.be.equal(1) logs[0].event.should.be.equal('Rewarded') @@ -138,40 +268,272 @@ contract('BlockReward', async (accounts) => { rewards[0].should.be.bignumber.equal(expectedRewardForValidator) }) it('reward amount should update after BLOCKS_PER_YEAR and total yearly inflation should be calculated correctly', async () => { + const validator = accounts[0] await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) - + await consensus.setTotalStakeAmountMock(0) + await consensus.sendTransaction({ from: validator, value: minStakeAmount }).should.be.fulfilled let decimals = await blockReward.DECIMALS() let initialSupply = await blockReward.getTotalSupply() let blocksPerYear = await blockReward.getBlocksPerYear() let inflation = await blockReward.getInflation() let blockRewardAmount = await blockReward.getBlockRewardAmount() - // console.log(`initialSupply: ${initialSupply.div(decimals).toNumber()}, blockRewardAmount: ${blockRewardAmount.div(decimals).toNumber()}`) // each of the following calls advances a block let i = 0 let blockNumber = await web3.eth.getBlockNumber() while (blockNumber % BLOCKS_PER_YEAR !== 0) { // console.log('block #', blockNumber) - await blockReward.reward([accounts[3]], [0], {from: mockSystemAddress}).should.be.fulfilled + await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled blockNumber = await web3.eth.getBlockNumber() i++ } - // console.log('i', i) let totalSupply = await blockReward.getTotalSupply() let newBlockRewardAmount = await blockReward.getBlockRewardAmount() - // console.log(`totalSupply: ${totalSupply.div(decimals).toNumber()}, newBlockRewardAmount: ${newBlockRewardAmount.div(decimals).toNumber()}`) let expectedSupply = initialSupply for (let j = 0; j < i; j++) { expectedSupply = expectedSupply.add(blockRewardAmount) } - // console.log(`expectedSupply: ${expectedSupply.div(decimals).toNumber()}`) totalSupply.should.be.bignumber.equal(expectedSupply) newBlockRewardAmount.should.be.bignumber.equal((totalSupply.mul(decimals).mul(inflation).div(toBN(100))).div(blocksPerYear).div(decimals)) }) + + it('call reward with 0 blockReward', async () => { + const validator = accounts[4] + await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.setTotalStakeAmountMock(minStakeAmount) + let {logs} = await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled + + ZERO.should.be.bignumber.equal(await blockReward.getBlockRewardAmountPerValidator(validator)) + + // await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled + + // let initialSupply = await blockReward.getTotalSupply() + // let blockRewardAmount = await blockReward.getBlockRewardAmountPerValidator(validator) + // let {logs} = await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled + }) + + describe('custom', async () =>{ + const validator = accounts[3] + beforeEach(async () => { + // Consensus + consensusImpl = await Consensus.new() + proxy = await EternalStorageProxy.new(ZERO_ADDRESS, consensusImpl.address) + consensus = await Consensus.at(proxy.address) + await consensus.initialize(owner) + + // ProxyStorage + proxyStorageImpl = await ProxyStorage.new() + proxy = await EternalStorageProxy.new(ZERO_ADDRESS, proxyStorageImpl.address) + proxyStorage = await ProxyStorage.at(proxy.address) + await proxyStorage.initialize(consensus.address) + await consensus.setProxyStorage(proxyStorage.address) + + // BlockReward + blockRewardImpl = await BlockReward.new() + proxy = await EternalStorageProxy.new(proxyStorage.address, blockRewardImpl.address) + blockReward = await BlockReward.at(proxy.address) + await blockReward.initialize(toWei(toBN(300000000000000000 || 0), 'gwei')) + + // Voting + votingImpl = await Voting.new() + proxy = await EternalStorageProxy.new(proxyStorage.address, votingImpl.address) + voting = await Voting.at(proxy.address) + + // Initialize ProxyStorage + await proxyStorage.initializeAddresses( + blockReward.address, + voting.address + ) + let ballotLimitPerValidator = (await voting.getBallotLimitPerValidator()).toNumber() + ballotLimitPerValidator.should.be.equal(Math.floor(100)) + await consensus.setNewValidatorSetMock([validator]) + await consensus.setFinalizedMock(false, {from: owner}) + await consensus.setSystemAddressMock(owner, {from: owner}) + await consensus.finalizeChange().should.be.fulfilled + + true.should.be.equal(await voting.isValidVotingKey(validator)) + }) + it('new test', async () => { + const validator = accounts[3]; + await voting.initialize().should.be.fulfilled + const minStakeAmount = await consensus.getMinStake(); + await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.setTotalStakeAmountMock(minStakeAmount) + await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled + await consensus.setCycleDurationBlocks(1); + + let proposedValue = RANDOM_ADDRESS + let contractType = CONTRACT_TYPES.CONSENSUS + const CYCLE_DURATION_BLOCKS = 120; + const voteCyclesDuration = 10 + let currentCycleEndBlock = await consensus.getCurrentCycleEndBlock() + let voteStartAfterNumberOfCycles = 1 + let voteStartAfterNumberOfBlocks = toBN(voteStartAfterNumberOfCycles).mul(toBN(CYCLE_DURATION_BLOCKS)) + let startBlock = currentCycleEndBlock.add(voteStartAfterNumberOfBlocks) + let voteEndAfterNumberOfBlocks = toBN(voteCyclesDuration).mul(toBN(CYCLE_DURATION_BLOCKS)) + let endBlock = startBlock.add(voteEndAfterNumberOfBlocks) + + let {logs} = await voting.newBallot( + voteStartAfterNumberOfCycles, + voteCyclesDuration, + contractType, + proposedValue, 'description', {from: validator}).should.be.fulfilled + console.log(logs[0].event); + logs[0].args['id'].should.be.bignumber.equal(toBN(0)) + logs[0].args['creator'].should.be.equal(validator) + + console.log('activeBallots',await voting.activeBallots.call()); + await consensus.setCycleDurationBlocks(1); + await consensus.setCurrentCycleEndBlock(5); + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + let block = await web3.eth.getBlock("latest") + console.log('block.number', block.number) + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + true.should.be.equal(await consensus.hasCycleEnded.call()) + console.log('currentValidatorsLength',await consensus.currentValidatorsLength.call()) + console.log('activeBallotsLength',await voting.activeBallotsLength.call()) + console.log('activeBallotsAtIndex',await voting.activeBallotsAtIndex(0)) + console.log('canBeFinalized',await voting.canBeFinalized(0)) + await voting.setAcceptedMock(0,3); + console.log('getAccepted',await voting.getAccepted(0)) + console.log('getRejected',await voting.getRejected(0)) + console.log('getContractType',await voting.getContractType(0)) + console.log('getProposedValue',await voting.getProposedValue(0)) + { + await voting.setBalotStartBlockMock(0, block.number -2); + await voting.setBalotEndBlockMock(0, block.number + 2); + console.log('getStartBlock', await voting.getStartBlock(0)) + console.log('getFinalizeCalled', await voting.getFinalizeCalled(0)) + console.log('canBeFinalized', await voting.canBeFinalized(0)) + await blockReward.reward([validator], [0], {from: mockSystemAddress}) + .then(utils.receiptShouldSucceed) + // .catch(utils.catchReceiptShouldFailed); + + // let {logs} = + + // console.log('****',logs.length); + // console.log('****',logs[0].event); + } + }) + it('new test2', async () => { + const validator = accounts[3]; + await voting.initialize().should.be.fulfilled + const minStakeAmount = await consensus.getMinStake(); + await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.setTotalStakeAmountMock(minStakeAmount) + await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled + await consensus.setCycleDurationBlocks(1); + + let proposedValue = RANDOM_ADDRESS + let contractType = CONTRACT_TYPES.PROXY_STORAGE + const voteCyclesDuration = 10 + let voteStartAfterNumberOfCycles = 1 - describe('emitRewardedOnCycle', function() { + let {logs} = await voting.newBallot( + voteStartAfterNumberOfCycles, + voteCyclesDuration, + contractType, + proposedValue, 'description', {from: validator}).should.be.fulfilled + console.log(logs[0].event); + logs[0].args['id'].should.be.bignumber.equal(toBN(0)) + logs[0].args['creator'].should.be.equal(validator) + + console.log('activeBallots',await voting.activeBallots.call()); + await consensus.setCycleDurationBlocks(1); + await consensus.setCurrentCycleEndBlock(5); + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + let block = await web3.eth.getBlock("latest") + console.log('block.number', block.number) + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + true.should.be.equal(await consensus.hasCycleEnded.call()) + console.log('currentValidatorsLength',await consensus.currentValidatorsLength.call()) + console.log('activeBallotsLength',await voting.activeBallotsLength.call()) + console.log('activeBallotsAtIndex',await voting.activeBallotsAtIndex(0)) + console.log('canBeFinalized',await voting.canBeFinalized(0)) + await voting.setAcceptedMock(0,3); + console.log('getAccepted',await voting.getAccepted(0)) + console.log('getRejected',await voting.getRejected(0)) + console.log('getContractType',await voting.getContractType(0)) + console.log('getProposedValue',await voting.getProposedValue(0)) + { + await voting.setBalotStartBlockMock(0, block.number -2); + await voting.setBalotEndBlockMock(0, block.number + 2); + console.log('getStartBlock', await voting.getStartBlock(0)) + console.log('getFinalizeCalled', await voting.getFinalizeCalled(0)) + console.log('canBeFinalized', await voting.canBeFinalized(0)) + // await blockReward.reward([validator], [0], {from: mockSystemAddress}) + // .then(utils.receiptShouldSucceed) + } + }) + it('new test3', async () => { + const validator = accounts[3]; + await voting.initialize().should.be.fulfilled + const minStakeAmount = await consensus.getMinStake(); + await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) + await consensus.setTotalStakeAmountMock(minStakeAmount) + await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled + await consensus.setCycleDurationBlocks(1); + + let proposedValue = RANDOM_ADDRESS + let contractType = CONTRACT_TYPES.VOTING + const CYCLE_DURATION_BLOCKS = 120; + const voteCyclesDuration = 10 + let currentCycleEndBlock = await consensus.getCurrentCycleEndBlock() + let voteStartAfterNumberOfCycles = 1 + let voteStartAfterNumberOfBlocks = toBN(voteStartAfterNumberOfCycles).mul(toBN(CYCLE_DURATION_BLOCKS)) + let startBlock = currentCycleEndBlock.add(voteStartAfterNumberOfBlocks) + let voteEndAfterNumberOfBlocks = toBN(voteCyclesDuration).mul(toBN(CYCLE_DURATION_BLOCKS)) + let endBlock = startBlock.add(voteEndAfterNumberOfBlocks) + + let {logs} = await voting.newBallot( + voteStartAfterNumberOfCycles, + voteCyclesDuration, + contractType, + proposedValue, 'description', {from: validator}).should.be.fulfilled + console.log(logs[0].event); + logs[0].args['id'].should.be.bignumber.equal(toBN(0)) + logs[0].args['creator'].should.be.equal(validator) + + console.log('activeBallots',await voting.activeBallots.call()); + await consensus.setCycleDurationBlocks(1); + await consensus.setCurrentCycleEndBlock(5); + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + let block = await web3.eth.getBlock("latest") + console.log('block.number', block.number) + console.log('getCurrentCycleEndBlock', toBN(await consensus.getCurrentCycleEndBlock.call()).toString()); + true.should.be.equal(await consensus.hasCycleEnded.call()) + console.log('currentValidatorsLength',await consensus.currentValidatorsLength.call()) + console.log('activeBallotsLength',await voting.activeBallotsLength.call()) + console.log('activeBallotsAtIndex',await voting.activeBallotsAtIndex(0)) + console.log('canBeFinalized',await voting.canBeFinalized(0)) + await voting.setAcceptedMock(0,3); + console.log('getAccepted',await voting.getAccepted(0)) + console.log('getRejected',await voting.getRejected(0)) + console.log('getContractType',await voting.getContractType(0)) + console.log('getProposedValue',await voting.getProposedValue(0)) + { + await voting.setBalotStartBlockMock(0, block.number -2); + await voting.setBalotEndBlockMock(0, block.number + 2); + console.log('getStartBlock', await voting.getStartBlock(0)) + console.log('getFinalizeCalled', await voting.getFinalizeCalled(0)) + console.log('canBeFinalized', await voting.canBeFinalized(0)) + await blockReward.reward([validator], [0], {from: mockSystemAddress}) + .then(utils.receiptShouldSucceed) + // .catch(utils.catchReceiptShouldFailed); + + // let {logs} = + + // console.log('****',logs.length); + // console.log('****',logs[0].event); + } + }) + }); + + }) + + describe('emitRewardedOnCycle', function () { beforeEach(async () => { await blockReward.initialize(INITIAL_SUPPLY) }) @@ -199,10 +561,13 @@ contract('BlockReward', async (accounts) => { let BLOCKS_TO_REWARD = 10 let blockRewardAmount = await blockReward.getBlockRewardAmount() let expectedAmount = blockRewardAmount.mul(toBN(BLOCKS_TO_REWARD)) + let minStakeAmount = await consensus.getMinStake() + const validator = accounts[0] + await consensus.sendTransaction({from: validator, value: minStakeAmount}).should.be.fulfilled await blockReward.setSystemAddressMock(mockSystemAddress, {from: owner}) for (let i = 0; i < BLOCKS_TO_REWARD; i++) { - await blockReward.reward([accounts[3]], [0], {from: mockSystemAddress}).should.be.fulfilled + await blockReward.reward([validator], [0], {from: mockSystemAddress}).should.be.fulfilled } await blockReward.setShouldEmitRewardedOnCycleMock(true) diff --git a/test/consensus.test.js b/test/consensus.test.js index ca4faa0..7eda548 100644 --- a/test/consensus.test.js +++ b/test/consensus.test.js @@ -1,23 +1,31 @@ +/* eslint-disable prefer-const */ +/* eslint-disable object-curly-spacing */ const Consensus = artifacts.require('ConsensusMock.sol') const ProxyStorage = artifacts.require('ProxyStorageMock.sol') const EternalStorageProxy = artifacts.require('EternalStorageProxyMock.sol') -const BlockReward = artifacts.require('BlockReward.sol') +const BlockReward = artifacts.require('BlockRewardMock.sol') const Voting = artifacts.require('Voting.sol') -const {ERROR_MSG, ZERO_AMOUNT, SYSTEM_ADDRESS, ZERO_ADDRESS, RANDOM_ADDRESS, advanceBlocks} = require('./helpers') -const {toBN, toWei, toChecksumAddress} = web3.utils +const { ERROR_MSG, ZERO_AMOUNT, SYSTEM_ADDRESS, ZERO_ADDRESS, RANDOM_ADDRESS, advanceBlocks, FIVE } = require('./helpers') +const { ZERO, ONE, TWO, THREE, FOUR } = require('./helpers') +const { toBN, toWei, toChecksumAddress } = web3.utils const MAX_VALIDATORS = 100 const MIN_STAKE_AMOUNT = 10000 +const MAX_STAKE_AMOUNT = 50000 const MULTIPLY_AMOUNT = 3 const MIN_STAKE = toWei(toBN(MIN_STAKE_AMOUNT), 'ether') -const ONE_ETHER = toWei(toBN(1), 'ether') +const MAX_STAKE = toWei(toBN(MAX_STAKE_AMOUNT), 'ether') +const ONE_ETHER = toWei(ONE, 'ether') +const TWO_ETHER = toWei(TWO, 'ether') +const ONE_WEI = ONE const LESS_THAN_MIN_STAKE = toWei(toBN(MIN_STAKE_AMOUNT - 1), 'ether') const MORE_THAN_MIN_STAKE = toWei(toBN(MIN_STAKE_AMOUNT + 1), 'ether') +const MORE_THAN_MAX_STAKE = toWei(toBN(MAX_STAKE_AMOUNT + 1), 'ether') const CYCLE_DURATION_BLOCKS = 120 const SNAPSHOTS_PER_CYCLE = 10 contract('Consensus', async (accounts) => { - let consensusImpl, proxy, consensus, blockReward, blockRewardAmount, decimals + let consensusImpl, proxy, proxyStorageImpl, proxyStorage, consensus, blockReward, blockRewardAmount, decimals, pendingValidators let owner = accounts[0] let nonOwner = accounts[1] let initialValidator = accounts[0] @@ -59,21 +67,31 @@ contract('Consensus', async (accounts) => { ) }) + const mockEoC = async () => { + await consensus.setNewValidatorSetMock(await consensus.pendingValidators()) + await consensus.setFinalizedMock(false, {from: owner}) + await consensus.setSystemAddressMock(owner, {from: owner}) + await consensus.finalizeChange().should.be.fulfilled + } + describe('initialize', async () => { it('default values', async () => { + ZERO.should.be.bignumber.equal(await consensus.currentValidatorsLength()) await consensus.initialize(initialValidator) await consensus.setProxyStorage(proxyStorage.address) owner.should.equal(await proxy.getOwner()) toChecksumAddress(SYSTEM_ADDRESS).should.be.equal(toChecksumAddress(await consensus.getSystemAddress())) true.should.be.equal(await consensus.isFinalized()) MIN_STAKE.should.be.bignumber.equal(await consensus.getMinStake()) + MAX_STAKE.should.be.bignumber.equal(await consensus.getMaxStake()) toBN(MAX_VALIDATORS).should.be.bignumber.equal(await consensus.getMaxValidators()) - toBN(CYCLE_DURATION_BLOCKS).should.be.bignumber.equal(await consensus.getCycleDurationBlocks()) toBN(SNAPSHOTS_PER_CYCLE).should.be.bignumber.equal(await consensus.getSnapshotsPerCycle()) - toBN(CYCLE_DURATION_BLOCKS / SNAPSHOTS_PER_CYCLE).should.be.bignumber.equal(await consensus.getBlocksToSnapshot()) + ZERO.should.be.bignumber.equal(await consensus.stakeAmount(initialValidator)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) false.should.be.equal(await consensus.hasCycleEnded()) - toBN(0).should.be.bignumber.equal(await consensus.getLastSnapshotTakenAtBlock()) - toBN(0).should.be.bignumber.equal(await consensus.getNextSnapshotId()) + ZERO.should.be.bignumber.equal(await consensus.getLastSnapshotTakenAtBlock()) + ZERO.should.be.bignumber.equal(await consensus.getNextSnapshotId()) + ONE.should.be.bignumber.equal(await consensus.currentValidatorsLength()) let validators = await consensus.getValidators() validators.length.should.be.equal(1) validators[0].should.be.equal(initialValidator) @@ -93,6 +111,26 @@ contract('Consensus', async (accounts) => { validators.length.should.be.equal(1) validators[0].should.be.equal(initialValidator) }) + + it('initial validator is added to pending after sending fuse', async () => { + await consensus.initialize(initialValidator) + await consensus.setProxyStorage(proxyStorage.address) + true.should.be.equal(await consensus.isFinalized()) + let validators = await consensus.getValidators() + validators.length.should.be.equal(1) + validators[0].should.be.equal(initialValidator) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + + await consensus.sendTransaction({from: initialValidator, value: MIN_STAKE}).should.be.fulfilled + + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(initialValidator) + + toBN(MIN_STAKE).should.be.bignumber.equal(await consensus.stakeAmount(initialValidator)) + toBN(MIN_STAKE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) describe('setProxyStorage', async () => { @@ -188,6 +226,46 @@ contract('Consensus', async (accounts) => { currentValidators.should.deep.equal(mockSet) logs[0].event.should.be.equal('ChangeFinalized') logs[0].args['newSet'].should.deep.equal(mockSet) + + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + context('with staking', () => { + it('adding validators should update the total stake', async () => { + let mockSet = [firstCandidate, secondCandidate] + + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.setNewValidatorSetMock(mockSet) + await consensus.setSystemAddressMock(accounts[0]) + + let {logs} = await consensus.finalizeChange().should.be.fulfilled + let currentValidators = await consensus.getValidators() + currentValidators.length.should.be.equal(2) + currentValidators.should.deep.equal(mockSet) + logs[0].event.should.be.equal('ChangeFinalized') + logs[0].args['newSet'].should.deep.equal(mockSet) + + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('removing validators should update the total stake', async () => { + let mockSet = [firstCandidate, secondCandidate] + + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.setNewValidatorSetMock(mockSet) + await consensus.setSystemAddressMock(accounts[0]) + + let {logs} = await consensus.finalizeChange().should.be.fulfilled + let currentValidators = await consensus.getValidators() + currentValidators.length.should.be.equal(2) + currentValidators.should.deep.equal(mockSet) + logs[0].event.should.be.equal('ChangeFinalized') + logs[0].args['newSet'].should.deep.equal(mockSet) + + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) }) @@ -200,78 +278,431 @@ contract('Consensus', async (accounts) => { it('should not allow zero stake', async () => { await consensus.send(0, {from: firstCandidate}).should.be.rejectedWith(ERROR_MSG) }) - it('less than minimum stake - should not be added to pending validators', async () => { - await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled - // contract balance should be updated - LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - // sender stake amount should be updated - LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) - // pending validators should not be updated - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(0) - // validator fee should not be set - toBN(0).should.be.bignumber.equal(await consensus.validatorFee(firstCandidate)) - }) - it('minimum stake amount', async () => { - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled - MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(1) - pendingValidators[0].should.be.equal(firstCandidate) - // default validator fee should be set - let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() - defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(firstCandidate)) + + context('with the initial validator', () => { + const validator = initialValidator + it('less than minimum stake, should update the total stake', async () => { + await consensus.sendTransaction({from: validator, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + // contract balance should be updated + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + // sender stake amount should be updated + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + // total stake is updated because the validator in the current validator + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // pending validators should not be updated + // (the validator is not added to pending validators fot next cycle) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + // validator fee should not be set + ZERO.should.be.bignumber.equal(await consensus.validatorFee(validator)) + }) + it('minimum stake amount', async () => { + await consensus.sendTransaction({from: validator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + }) + it('should allow more than minimum stake', async () => { + await consensus.sendTransaction({from: validator, value: MORE_THAN_MIN_STAKE}).should.be.fulfilled + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + }) + it('should allow the maximum stake', async () => { + ZERO.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + await consensus.sendTransaction({from: validator, value: MAX_STAKE}).should.be.fulfilled + MAX_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MAX_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + }) + + it('should not allow more the maximum stake', async () => { + await consensus.sendTransaction({from: validator, value: MORE_THAN_MAX_STAKE}).should.be.rejectedWith(ERROR_MSG) + }) }) - it('should not allow more than minimum stake', async () => { - await consensus.sendTransaction({from: firstCandidate, value: MORE_THAN_MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + + context('with first candidate', () => { + const validator = firstCandidate + it('less than minimum stake - should not be added to pending validators', async () => { + await consensus.sendTransaction({from: validator, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + // contract balance should be updated + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + // sender stake amount should be updated + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // pending validators should not be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + // validator fee should not be set + ZERO.should.be.bignumber.equal(await consensus.validatorFee(validator)) + + await mockEoC() + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + it('minimum stake amount', async () => { + let validatorsLength = await consensus.currentValidatorsLength() + validatorsLength.should.be.bignumber.equal(ONE) + true.should.be.equal(await consensus.isValidator(initialValidator)) + + await consensus.sendTransaction({from: validator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + validatorsLength.should.be.bignumber.equal(ONE) + true.should.be.equal(await consensus.isValidator(validator)) + }) + it('should allow more than minimum stake', async () => { + await consensus.sendTransaction({from: validator, value: MORE_THAN_MIN_STAKE}).should.be.fulfilled + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + + // total stake is updated on the EoC + await mockEoC() + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + it('should allow the maximum stake', async () => { + ZERO.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + await consensus.sendTransaction({from: validator, value: MAX_STAKE}).should.be.fulfilled + MAX_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MAX_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(validator)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(validator) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(validator)) + + // total stake is updated on the EoC + await mockEoC() + MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('should not allow more the maximum stake', async () => { + await consensus.sendTransaction({from: validator, value: MORE_THAN_MAX_STAKE}).should.be.rejectedWith(ERROR_MSG) + }) }) }) describe('advanced', async () => { - it('minimum stake amount, in more than one transaction', async () => { - // 1st stake - await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled - LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(0) + context('with first candidate', () => { + it('minimum stake amount, in more than one transaction', async () => { + // 1st stake + await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) - // 2nd stake - await consensus.sendTransaction({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled - MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(1) - pendingValidators[0].should.be.equal(firstCandidate) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + + // 2nd stake + await consensus.sendTransaction({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('more than minimum stake amount, in more than one transaction', async () => { + // 1st stake + await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + + // 2nd stake + await consensus.sendTransaction({from: firstCandidate, value: TWO_ETHER}).should.be.fulfilled + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // total stake is updated on start of the cycle + await mockEoC() + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('more than one validator', async () => { + // add 1st validator + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + // add 2nd validator + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(2) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + it('multiple validators, multiple times', async () => { + const expectedValidators = [] + // add 1st validator + expectedValidators.push(firstCandidate) + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // add 2nd validator + expectedValidators.push(secondCandidate) + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + ZERO.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // doubling the stake for the 1st validator + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.mul(THREE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // doubling the stake for the 2st validator + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + MIN_STAKE.mul(FOUR).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // total stake does not change of the EoC + await mockEoC() + MIN_STAKE.mul(FOUR).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) - it('more than one validator', async () => { - // add 1st validator - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(1) - // add 2nd validator - await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(2) + + context('with existing stake', () => { + beforeEach(async () => { + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + await mockEoC() + }) + + it('minimum stake amount, in more than one transaction', async () => { + // 1st stake + await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // 2nd stake + await consensus.sendTransaction({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('more than minimum stake amount, in more than one transaction', async () => { + // 1st stake + await consensus.sendTransaction({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.add(LESS_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // 2nd stake + await consensus.sendTransaction({from: firstCandidate, value: TWO_ETHER}).should.be.fulfilled + MIN_STAKE.add(MORE_THAN_MIN_STAKE).should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.add(MORE_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.add(MORE_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + // total stake is updated on start of the cycle + await mockEoC() + MIN_STAKE.add(MORE_THAN_MIN_STAKE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('more than one validator', async () => { + // add 1st validator + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + // add 2nd validator + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(2) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.mul(THREE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('multiple validators, multiple times', async () => { + const expectedValidators = [] + // add 1st validator + expectedValidators.push(firstCandidate) + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + // add 2nd validator + expectedValidators.push(secondCandidate) + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // total stake is updated on the EoC + await mockEoC() + MIN_STAKE.mul(THREE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // doubling the stake for the 1st validator + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(THREE).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.mul(FOUR).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // doubling the stake for the 2st validator + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + MIN_STAKE.mul(FIVE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // total stake does not change of the EoC + await mockEoC() + MIN_STAKE.mul(FIVE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) - it('multiple validators, multiple times', async () => { - let expectedValidators = [] - // add 1st validator - expectedValidators.push(firstCandidate) - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(expectedValidators.length) - pendingValidators.should.deep.equal(expectedValidators) - // add 2nd validator - expectedValidators.push(secondCandidate) - await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(expectedValidators.length) - pendingValidators.should.deep.equal(expectedValidators) - // try to add 1st validator one more time - should reject - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) - // try to add 2nd validator one more time - should reject - await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + + context('total stake', () => { + it('two validators', async () => { + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE.mul(FOUR)}).should.be.fulfilled + + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await mockEoC() + + MIN_STAKE.mul(FIVE).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await consensus.sendTransaction({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled + + MIN_STAKE.mul(FIVE).add(ONE_ETHER).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await consensus.sendTransaction({from: thirdCandidate, value: ONE_ETHER}).should.be.fulfilled + ONE_ETHER.should.be.bignumber.equal(await consensus.stakeAmount(thirdCandidate)) + + MIN_STAKE.mul(FIVE).add(ONE_ETHER).should.be.bignumber.equal(await consensus.totalStakeAmount()) + await mockEoC() + + MIN_STAKE.mul(FIVE).add(ONE_ETHER).should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('validator with delegators', async () => { + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + await consensus.delegate(firstCandidate, {from: firstDelegator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await mockEoC() + + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await consensus.delegate(firstCandidate, {from: firstDelegator, value: ONE_ETHER}).should.be.fulfilled + await consensus.delegate(secondCandidate, {from: firstDelegator, value: ONE_ETHER}).should.be.fulfilled + + MIN_STAKE.mul(TWO).add(ONE_ETHER).should.be.bignumber.equal(await consensus.totalStakeAmount()) + MIN_STAKE.mul(TWO).add(ONE_ETHER).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ONE_ETHER.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + + await mockEoC() + + MIN_STAKE.mul(TWO).add(ONE_ETHER).should.be.bignumber.equal(await consensus.totalStakeAmount()) + MIN_STAKE.mul(TWO).add(ONE_ETHER).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ONE_ETHER.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + }) }) }) }) @@ -291,20 +722,86 @@ contract('Consensus', async (accounts) => { LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) // sender stake amount should be updated LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) // pending validators should not be updated let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) }) it('minimum stake amount', async () => { - await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + ONE.should.be.bignumber.equal(await consensus.currentValidatorsLength()) + let validators = await consensus.getValidators() + validators.length.should.be.equal(1) + validators[0].should.be.equal(initialValidator) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await consensus.stake({ from: firstCandidate, value: MIN_STAKE }).should.be.fulfilled MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // initial validator did not stake so he is not in the pending validators list + ONE.should.be.bignumber.equal(await consensus.currentValidatorsLength()) + validators = await consensus.getValidators() + validators.length.should.be.equal(1) + validators[0].should.be.equal(firstCandidate) + }) + + it('should allow more than minimum stake', async () => { + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + await consensus.stake({ from: firstCandidate, value: MORE_THAN_MIN_STAKE }).should.be.fulfilled + + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) pendingValidators[0].should.be.equal(firstCandidate) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(firstCandidate)) + + await mockEoC() + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // initial validator did not stake so he is not in the pending validators list + ONE.should.be.bignumber.equal(await consensus.currentValidatorsLength()) + let validators = await consensus.getValidators() + validators.length.should.be.equal(1) + validators[0].should.be.equal(firstCandidate) }) - it('should not allow more than minimum stake', async () => { - await consensus.stake({from: firstCandidate, value: MORE_THAN_MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + + it('should allow the maximum stake', async () => { + ZERO.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + await consensus.stake({from: firstCandidate, value: MAX_STAKE}).should.be.fulfilled + MAX_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MAX_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + // default validator fee should be set + let defaultValidatorFee = await consensus.DEFAULT_VALIDATOR_FEE() + defaultValidatorFee.should.be.bignumber.equal(await consensus.validatorFee(firstCandidate)) + + await mockEoC() + MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // initial validator did not stake so he is not in the pending validators list + ONE.should.be.bignumber.equal(await consensus.currentValidatorsLength()) + let validators = await consensus.getValidators() + validators.length.should.be.equal(1) + validators[0].should.be.equal(firstCandidate) + }) + + it('should not allow more the maximum stake', async () => { + await consensus.stake({from: firstCandidate, value: MORE_THAN_MAX_STAKE}).should.be.rejectedWith(ERROR_MSG) }) }) describe('advanced', async () => { @@ -313,6 +810,7 @@ contract('Consensus', async (accounts) => { await consensus.stake({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) @@ -320,58 +818,92 @@ contract('Consensus', async (accounts) => { await consensus.stake({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) pendingValidators[0].should.be.equal(firstCandidate) + + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) }) - it('more than one validator', async () => { - // add 1st validator - await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(1) - // add 2nd validator - await consensus.stake({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(2) - }) - it('multiple times according to staked amount, in more than one transaction', async () => { + + it('more than minimum stake amount, in more than one transaction', async () => { // 1st stake await consensus.stake({from: firstCandidate, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) - // 2nd stake - added once - let expectedValidators = [firstCandidate] - await consensus.stake({from: firstCandidate, value: ONE_ETHER}).should.be.fulfilled - MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + // 2nd stake + await consensus.stake({from: firstCandidate, value: TWO_ETHER}).should.be.fulfilled + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) pendingValidators[0].should.be.equal(firstCandidate) - // 3rd stake - should be rejected - await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + await mockEoC() + MORE_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('more than one validator', async () => { + // add 1st validator + await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + // add 2nd validator + await consensus.stake({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(2) + + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) }) + it('multiple validators, multiple times', async () => { let expectedValidators = [] // add 1st validator expectedValidators.push(firstCandidate) await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(expectedValidators.length) pendingValidators.should.deep.equal(expectedValidators) // add 2nd validator expectedValidators.push(secondCandidate) await consensus.stake({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + + // doubling the stake for the 1st validator + await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.fulfilled + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(expectedValidators.length) + pendingValidators.should.deep.equal(expectedValidators) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // doubling the stake for the 2st validator + await consensus.stake({from: secondCandidate, value: MIN_STAKE}).should.be.fulfilled pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(expectedValidators.length) pendingValidators.should.deep.equal(expectedValidators) - // try to add 1st validator one more time - should reject - await consensus.stake({from: firstCandidate, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) - // try to add 2nd validator one more time - should reject - await consensus.stake({from: secondCandidate, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + await mockEoC() + MIN_STAKE.mul(FOUR).should.be.bignumber.equal(await consensus.totalStakeAmount()) }) }) }) @@ -396,6 +928,7 @@ contract('Consensus', async (accounts) => { LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) // delegated amount should be updated LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) // pending validators should not be updated let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) @@ -415,6 +948,35 @@ contract('Consensus', async (accounts) => { MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) // delegated amount should be updated MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) + // total stake isn't updated until the validator is in the current set + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // pending validators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators[0].should.be.equal(firstCandidate) + // delegators list should be updated + let delegators = await consensus.delegators(firstCandidate) + delegators.length.should.be.equal(1) + delegators[0].should.be.equal(firstDelegator) + let delegatorsLength = await consensus.delegatorsLength(firstCandidate) + delegatorsLength.should.be.bignumber.equal(toBN(1)) + firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) + + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('should allow the maximum stake', async () => { + ZERO.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + await consensus.delegate(firstCandidate, {from: firstDelegator, value: MAX_STAKE}).should.be.fulfilled + + MAX_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + // sender stake amount should be updated + MAX_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + // MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // delegated amount should be updated + MAX_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) // pending validators should be updated let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) @@ -426,6 +988,13 @@ contract('Consensus', async (accounts) => { let delegatorsLength = await consensus.delegatorsLength(firstCandidate) delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) + + await mockEoC() + MAX_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) + + it('should not allow more the maximum stake', async () => { + await consensus.delegate(firstCandidate, {from: firstDelegator, value: MORE_THAN_MAX_STAKE}).should.be.rejectedWith(ERROR_MSG) }) }) describe('advanced', async () => { @@ -435,6 +1004,7 @@ contract('Consensus', async (accounts) => { LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) let delegators = await consensus.delegators(firstCandidate) @@ -449,6 +1019,7 @@ contract('Consensus', async (accounts) => { MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) + // MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) pendingValidators[0].should.be.equal(firstCandidate) @@ -458,10 +1029,16 @@ contract('Consensus', async (accounts) => { delegatorsLength = await consensus.delegatorsLength(firstCandidate) delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) + + await mockEoC() + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) it('more than one validator', async () => { // add 1st validator await consensus.delegate(firstCandidate, {from: firstDelegator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) // delegators list should be updated @@ -473,6 +1050,8 @@ contract('Consensus', async (accounts) => { firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) // add 2nd validator await consensus.delegate(secondCandidate, {from: firstDelegator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(secondCandidate)) + // MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(2) // delegators list should be updated @@ -482,12 +1061,17 @@ contract('Consensus', async (accounts) => { delegatorsLength = await consensus.delegatorsLength(secondCandidate) delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(secondCandidate, 0)) + + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) }) it('multiple times according to staked amount, in more than one transaction', async () => { // 1st stake await consensus.delegate(firstCandidate, {from: firstDelegator, value: LESS_THAN_MIN_STAKE}).should.be.fulfilled LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) @@ -503,6 +1087,7 @@ contract('Consensus', async (accounts) => { await consensus.delegate(firstCandidate, {from: firstDelegator, value: ONE_ETHER}).should.be.fulfilled MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) MIN_STAKE.should.be.bignumber.equal(await consensus.delegatedAmount(firstDelegator, firstCandidate)) pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(1) @@ -514,7 +1099,9 @@ contract('Consensus', async (accounts) => { delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) - await consensus.delegate(firstCandidate, {from: firstDelegator, value: MIN_STAKE}).should.be.rejectedWith(ERROR_MSG) + await consensus.delegate(firstCandidate, {from: firstDelegator, value: MIN_STAKE}).should.be.fulfilled + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + // MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) delegators = await consensus.delegators(firstCandidate) delegators.length.should.be.equal(1) delegators[0].should.be.equal(firstDelegator) @@ -532,99 +1119,36 @@ contract('Consensus', async (accounts) => { }) it('hasCycleEnded', async () => { false.should.be.equal(await consensus.hasCycleEnded()) + let currentBlockNumber = await web3.eth.getBlockNumber() let currentCycleEndBlock = await consensus.getCurrentCycleEndBlock() let blocksToAdvance = currentCycleEndBlock.toNumber() - currentBlockNumber - await advanceBlocks(blocksToAdvance - 1) + + await advanceBlocks(blocksToAdvance) true.should.be.equal(await consensus.hasCycleEnded()) }) - it('shouldTakeSnapshot', async () => { - let blocksToSnapshot = await consensus.getBlocksToSnapshot() - let lastSnapshotTakenAtBlock = await consensus.getLastSnapshotTakenAtBlock() - let currentBlockNumber = toBN(await web3.eth.getBlockNumber()) - let shouldTakeSnapshot = (currentBlockNumber.sub(lastSnapshotTakenAtBlock)).gte(blocksToSnapshot) - shouldTakeSnapshot.should.be.equal(await consensus.shouldTakeSnapshot()) - }) - it('getRandom', async () => { - let repeats = 25 - let randoms = [] - for (let i = 0; i < repeats; i++) { - randoms.push((await consensus.getRandom(0, SNAPSHOTS_PER_CYCLE)).toNumber()) - await advanceBlocks(1) - } - randoms.length.should.be.equal(repeats) - let distincts = [...new Set(randoms)] - distincts.length.should.be.greaterThan(1) - distincts.length.should.be.most(SNAPSHOTS_PER_CYCLE) + + it('cycle', async () => { + false.should.be.equal(await consensus.hasCycleEnded()) + + let currentBlockNumber = await web3.eth.getBlockNumber() + let currentCycleEndBlock = await consensus.getCurrentCycleEndBlock() + let blocksToAdvance = currentCycleEndBlock.toNumber() - currentBlockNumber + // console.log({ currentBlockNumber, currentCycleEndBlock: currentCycleEndBlock.toNumber(), blocksToAdvance }) + await advanceBlocks(blocksToAdvance) + true.should.be.equal(await consensus.hasCycleEnded()) + + await consensus.sendTransaction({from: initialValidator, value: MIN_STAKE}).should.be.fulfilled + + await blockReward.cycleMock().should.be.fulfilled + false.should.be.equal(await consensus.hasCycleEnded()) }) + it('cycle function should only be called by BlockReward', async () => { await consensus.cycle().should.be.rejectedWith(ERROR_MSG) await proxyStorage.setBlockRewardMock(owner) await consensus.cycle().should.be.fulfilled }) - it('snapshot with less validators than MAX_VALIDATORS - entire set should be saved', async () => { - let expectedValidators = [] - for (let i = 1; i <= MAX_VALIDATORS - 1; i++) { - await consensus.sendTransaction({from: accounts[i-1], value: MIN_STAKE}).should.be.fulfilled - expectedValidators.push(accounts[i-1]) - } - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(expectedValidators.length) - pendingValidators.should.deep.equal(expectedValidators) - let blocksToSnapshot = await consensus.getBlocksToSnapshot() - let snapshotId = await consensus.getNextSnapshotId() - await advanceBlocks(blocksToSnapshot) - await proxyStorage.setBlockRewardMock(owner) - await consensus.cycle().should.be.fulfilled - let snapshot = await consensus.getSnapshotAddresses(snapshotId) - snapshot.length.should.be.equal(expectedValidators.length) - snapshot.forEach(address => { - expectedValidators.splice(expectedValidators.indexOf(address), 1) - }) - expectedValidators.length.should.be.equal(0) - }) - it('snapshot with exactly MAX_VALIDATORS validators - entire set should be saved', async () => { - let expectedValidators = [] - for (let i = 1; i <= MAX_VALIDATORS; i++) { - await consensus.sendTransaction({from: accounts[i-1], value: MIN_STAKE}).should.be.fulfilled - expectedValidators.push(accounts[i-1]) - } - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(expectedValidators.length) - pendingValidators.should.deep.equal(expectedValidators) - let blocksToSnapshot = await consensus.getBlocksToSnapshot() - let snapshotId = await consensus.getNextSnapshotId() - await advanceBlocks(blocksToSnapshot) - await proxyStorage.setBlockRewardMock(owner) - await consensus.cycle().should.be.fulfilled - let snapshot = await consensus.getSnapshotAddresses(snapshotId) - snapshot.length.should.be.equal(expectedValidators.length) - snapshot.forEach(address => { - expectedValidators.splice(expectedValidators.indexOf(address), 1) - }) - expectedValidators.length.should.be.equal(0) - }) - it('snapshot with more validators than MAX_VALIDATORS - random set should be saved', async () => { - let expectedValidators = [] - for (let i = 1; i <= MAX_VALIDATORS + 1; i++) { - await consensus.sendTransaction({from: accounts[i-1], value: MIN_STAKE}).should.be.fulfilled - expectedValidators.push(accounts[i-1]) - } - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(expectedValidators.length) - pendingValidators.should.deep.equal(expectedValidators) - let blocksToSnapshot = await consensus.getBlocksToSnapshot() - let snapshotId = await consensus.getNextSnapshotId() - await advanceBlocks(blocksToSnapshot) - await proxyStorage.setBlockRewardMock(owner) - await consensus.cycle().should.be.fulfilled - let snapshot = await consensus.getSnapshotAddresses(snapshotId) - snapshot.length.should.be.equal(MAX_VALIDATORS) - snapshot.forEach(address => { - expectedValidators.splice(expectedValidators.indexOf(address), 1) - }) - expectedValidators.length.should.be.equal(pendingValidators.length - MAX_VALIDATORS) - }) }) describe('withdraw', async () => { @@ -633,6 +1157,7 @@ contract('Consensus', async (accounts) => { await consensus.setProxyStorage(proxyStorage.address) }) describe('stakers', async () => { + it('cannot withdraw zero', async () => { await consensus.methods['withdraw(uint256)'](ZERO_AMOUNT, {from: firstCandidate}).should.be.rejectedWith(ERROR_MSG) }) @@ -640,48 +1165,150 @@ contract('Consensus', async (accounts) => { await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) await consensus.methods['withdraw(uint256)'](MORE_THAN_MIN_STAKE).should.be.rejectedWith(ERROR_MSG) }) - it('can withdraw all staked amount', async () => { - // stake - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) - // stake - await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}) - // withdraw - await consensus.methods['withdraw(uint256)'](MIN_STAKE, {from: firstCandidate}) - MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - ZERO_AMOUNT.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) - // pendingValidators should be updated - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(1) - pendingValidators.should.deep.equal([secondCandidate]) - }) - it('can withdraw less than staked amount', async () => { - // stake - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) - // withdraw - await consensus.methods['withdraw(uint256)'](ONE_ETHER, {from: firstCandidate}) - let expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 1), 'ether') - let expectedValidators = [] - expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - // pendingValidators should be updated - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(0) + + describe('stakers are not curent validators', () => { + it('can withdraw all staked amount', async () => { + // stake + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) + // stake + await consensus.sendTransaction({from: secondCandidate, value: MIN_STAKE}) + + // await mockEoC() + // withdraw + await consensus.methods['withdraw(uint256)'](MIN_STAKE, {from: firstCandidate}) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + ZERO_AMOUNT.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + // MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // pendingValidators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + pendingValidators.should.deep.equal([secondCandidate]) + }) + it('can withdraw less than staked amount', async () => { + // stake + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) + // withdraw + await consensus.methods['withdraw(uint256)'](ONE_ETHER, {from: firstCandidate}) + LESS_THAN_MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + let expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 1), 'ether') + let expectedValidators = [] + expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // pendingValidators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + }) + it('can withdraw multiple times', async () => { + // stake + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + // withdraw 1st time + await consensus.methods['withdraw(uint256)'](ONE_ETHER, {from: firstCandidate}) + let expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 1), 'ether') + expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + expectedAmount.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + // withdraw 2nd time + await consensus.withdraw(ONE_ETHER, {from: firstCandidate}) + expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 2), 'ether') + expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + ZERO.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) - it('can withdraw multiple times', async () => { - // stake - await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) - // withdraw 1st time - await consensus.methods['withdraw(uint256)'](ONE_ETHER, {from: firstCandidate}) - let expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 1), 'ether') - let expectedValidators = [] - expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - let pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(0) - // withdraw 2nd time - await consensus.withdraw(ONE_ETHER, {from: firstCandidate}) - expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 2), 'ether') - expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) - pendingValidators = await consensus.pendingValidators() - pendingValidators.length.should.be.equal(0) + + describe('stakers are curent validators', () => { + it('cannot withdraw the min stake for active validator', async () => { + // stake + // console.log(await web3.eth.getBalance(firstCandidate)) + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + + await mockEoC() + // withdraw + const firstCandidateBalance = toBN(await web3.eth.getBalance(firstCandidate)) + await consensus.methods['withdraw(uint256)'](MIN_STAKE, {from: firstCandidate}).should.be.fulfilled + // firstCandidate doesn't get the stake back + firstCandidateBalance.should.be.bignumber.above(toBN(await web3.eth.getBalance(firstCandidate))) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + + // pendingValidators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + }) + + it('cannot withdraw one wei from the min stake for active validator', async () => { + // stake + // console.log(await web3.eth.getBalance(firstCandidate)) + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE}) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + + await mockEoC() + // withdraw + // console.log(await web3.eth.getBalance(firstCandidate)) + const firstCandidateBalance = toBN(await web3.eth.getBalance(firstCandidate)) + await consensus.methods['withdraw(uint256)'](ONE_WEI, {from: firstCandidate}).should.be.fulfilled + // firstCandidate doesn't get the stake back + firstCandidateBalance.should.be.bignumber.above(toBN(await web3.eth.getBalance(firstCandidate))) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + + // pendingValidators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(0) + }) + + it('can withdraw until the min stake for active validator', async () => { + await consensus.sendTransaction({from: firstCandidate, value: MIN_STAKE.mul(TWO)}) + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + + await mockEoC() + MIN_STAKE.mul(TWO).should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // withdraw + // console.log(await web3.eth.getBalance(firstCandidate)) + await consensus.methods['withdraw(uint256)'](MIN_STAKE, {from: firstCandidate}).should.be.fulfilled + // console.log(await web3.eth.getBalance(firstCandidate)) + + MIN_STAKE.should.be.bignumber.not.equal(toBN(await web3.eth.getBalance(firstCandidate))) + MIN_STAKE.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + MIN_STAKE.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + MIN_STAKE.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // pendingValidators should be updated + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + }) + + it('can withdraw multiple times', async () => { + // stake + const stake = MIN_STAKE.mul(TWO) + await consensus.sendTransaction({from: firstCandidate, value: stake}) + stake.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + + await mockEoC() + stake.should.be.bignumber.equal(await consensus.totalStakeAmount()) + + // withdraw 1st time + await consensus.methods['withdraw(uint256)'](ONE_ETHER, {from: firstCandidate}) + let expectedAmount = stake.sub(ONE_ETHER) + expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + expectedAmount.should.be.bignumber.equal(await consensus.stakeAmount(firstCandidate)) + let pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + // withdraw 2nd time + await consensus.withdraw(ONE_ETHER, {from: firstCandidate}) + // expectedAmount = toWei(toBN(MIN_STAKE_AMOUNT - 2), 'ether') + expectedAmount = stake.sub(TWO_ETHER) + expectedAmount.should.be.bignumber.equal(await web3.eth.getBalance(consensus.address)) + pendingValidators = await consensus.pendingValidators() + pendingValidators.length.should.be.equal(1) + expectedAmount.should.be.bignumber.equal(await consensus.totalStakeAmount()) + }) }) }) describe('delegators', async () => { @@ -736,10 +1363,10 @@ contract('Consensus', async (accounts) => { let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) // delegators list should be updated - delegators = await consensus.delegators(firstCandidate) + let delegators = await consensus.delegators(firstCandidate) delegators.length.should.be.equal(1) delegators[0].should.be.equal(firstDelegator) - delegatorsLength = await consensus.delegatorsLength(firstCandidate) + let delegatorsLength = await consensus.delegatorsLength(firstCandidate) delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) }) @@ -754,10 +1381,10 @@ contract('Consensus', async (accounts) => { let pendingValidators = await consensus.pendingValidators() pendingValidators.length.should.be.equal(0) // delegators list should be updated - delegators = await consensus.delegators(firstCandidate) + let delegators = await consensus.delegators(firstCandidate) delegators.length.should.be.equal(1) delegators[0].should.be.equal(firstDelegator) - delegatorsLength = await consensus.delegatorsLength(firstCandidate) + let delegatorsLength = await consensus.delegatorsLength(firstCandidate) delegatorsLength.should.be.bignumber.equal(toBN(1)) firstDelegator.should.be.equal(await consensus.delegatorsAtPosition(firstCandidate, 0)) // withdraw 2nd time diff --git a/test/contracts/ConsensusMock.sol b/test/contracts/ConsensusMock.sol deleted file mode 100644 index 36a9058..0000000 --- a/test/contracts/ConsensusMock.sol +++ /dev/null @@ -1,58 +0,0 @@ -pragma solidity ^0.4.24; - -import "../../contracts/Consensus.sol"; - -contract ConsensusMock is Consensus { - function setSystemAddressMock(address _newAddress) public onlyOwner { - addressStorage[SYSTEM_ADDRESS] = _newAddress; - } - - function getSystemAddress() public view returns(address) { - return addressStorage[SYSTEM_ADDRESS]; - } - - function hasCycleEnded() public view returns(bool) { - return _hasCycleEnded(); - } - - function shouldTakeSnapshot() public view returns(bool) { - return _shouldTakeSnapshot(); - } - - function getRandom(uint256 _from, uint256 _to) public view returns(uint256) { - return _getRandom(_from, _to); - } - - function getBlocksToSnapshot() public pure returns(uint256) { - return _getBlocksToSnapshot(); - } - - function setNewValidatorSetMock(address[] _newSet) public { - addressArrayStorage[NEW_VALIDATOR_SET] = _newSet; - } - - function setFinalizedMock(bool _status) public { - boolStorage[IS_FINALIZED] = _status; - } - - function setShouldEmitInitiateChangeMock(bool _status) public { - boolStorage[SHOULD_EMIT_INITIATE_CHANGE] = _status; - } - - function getMinStake() public pure returns(uint256) { - return 1e22; - } - - function getCycleDurationBlocks() public pure returns(uint256) { - return 120; - } - - function getSnapshotsPerCycle() public pure returns(uint256) { - return 10; - } - - function setValidatorFeeMock(uint256 _amount) external { - require (_amount <= 1 * DECIMALS); - _setValidatorFee(msg.sender, _amount); - } -} diff --git a/test/contracts/VotingMock.sol b/test/contracts/VotingMock.sol deleted file mode 100644 index 721f71c..0000000 --- a/test/contracts/VotingMock.sol +++ /dev/null @@ -1,10 +0,0 @@ -pragma solidity ^0.4.24; - -import "../../contracts/Voting.sol"; - -contract VotingMock is Voting { - - function setNextBallotIdMock(uint256 _id) public { - uintStorage[NEXT_BALLOT_ID] = _id; - } -} diff --git a/test/helpers.js b/test/helpers.js index 4f91b71..494a2c9 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -13,6 +13,14 @@ exports.ERROR_MSG_OPCODE = 'VM Exception while processing transaction: invalid o exports.INVALID_ARGUMENTS = 'Invalid number of arguments to Solidity function' exports.RANDOM_ADDRESS = '0xc0ffee254729296a45a3885639AC7E10F9d54979' +exports.ZERO = toBN(0) +exports.ONE = toBN(1) +exports.TWO = toBN(2) +exports.THREE = toBN(3) +exports.FOUR = toBN(4) +exports.FIVE = toBN(5) +exports.TEN = toBN(10) + exports.advanceTime = (seconds) => { return new Promise((resolve, reject) => { web3.currentProvider.send({ @@ -53,4 +61,4 @@ exports.advanceBlocks = (n) => { resolve(results[results.length - 1]) }) }) -} \ No newline at end of file +} diff --git a/test/proxyStorage.test.js b/test/proxyStorage.test.js index 547e244..6712205 100644 --- a/test/proxyStorage.test.js +++ b/test/proxyStorage.test.js @@ -119,4 +119,39 @@ contract('ProxyStorage', async (accounts) => { true.should.be.equal(await proxyStorageNew.isInitialized()) }) }) + describe('Ownership', async () => { + let proxyStorageNew + let proxyStorageStub = accounts[8] + beforeEach(async () => { + proxyStorageNew = await ProxyStorage.new() + await proxy.setProxyStorageMock(proxyStorageStub) + }) + it('transfer ownership', async () => { + await proxy.transferOwnership(accounts[1], {from: owner}).should.be.fulfilled + accounts[1].should.be.equal(await proxy.getOwner.call()) + }) + + it('remove owner', async () => { + await proxy.renounceOwnership({from: owner}).should.be.fulfilled + '0x0000000000000000000000000000000000000000'.should.be.equal(await proxy.getOwner.call()) + }) + }) + + describe('_finalizeBallot', async () => { + let proxyStorageNew + let proxyStorageStub = accounts[8] + beforeEach(async () => { + proxyStorageNew = await ProxyStorage.new() + await proxy.setProxyStorageMock(proxyStorageStub) + }) + it('transfer ownership', async () => { + await proxy.transferOwnership(accounts[1], {from: owner}).should.be.fulfilled + accounts[1].should.be.equal(await proxy.getOwner.call()) + }) + + it('remove owner', async () => { + await proxy.renounceOwnership({from: owner}).should.be.fulfilled + '0x0000000000000000000000000000000000000000'.should.be.equal(await proxy.getOwner.call()) + }) + }) }) diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..0fba724 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,478 @@ +var BN = require('bn.js'); + +var gasToUse = 0x47E7C4; + +function receiptShouldSucceed(result) { + return new Promise(function(resolve, reject) { + var receipt = web3.eth.getTransaction(result.tx); + + if(result.receipt.gasUsed == gasToUse) { + try { + assert.notEqual(result.receipt.gasUsed, gasToUse, "tx failed, used all gas"); + } + catch(err) { + reject(err); + } + } + else { + resolve(); + } + }); +} + +function receiptShouldFailed(result) { + return new Promise(function(resolve, reject) { + var receipt = web3.eth.getTransaction(result.tx); + + if(result.receipt.gasUsed == gasToUse) { + resolve(); + } + else { + try { + assert.equal(result.receipt.gasUsed, gasToUse, "tx succeed, used not all gas"); + } + catch(err) { + reject(err); + } + } + }); +} + +function catchReceiptShouldFailed(err) { + if (err.message.indexOf("invalid opcode") == -1 && err.message.indexOf("revert") == -1) { + throw err; + } +} + +function receiptShouldSucceedS(receipt) { + return new Promise(function(resolve, reject) { + if(receipt.gasUsed == gasToUse) { + try { + assert.notEqual(receipt.gasUsed, gasToUse, "tx failed, used all gas"); + } + catch(err) { + reject(err); + } + } + else { + resolve(); + } + }); +} + +function receiptShouldFailedS(receipt) { + return new Promise(function(resolve, reject) { + if(receipt.gasUsed == gasToUse) { + resolve(); + } + else { + try { + assert.equal(receipt.gasUsed, gasToUse, "tx succeed, used not all gas"); + } + catch(err) { + reject(err); + } + } + }); +} + + +function balanceShouldEqualTo(instance, address, expectedBalance, notCall) { + return new Promise(function(resolve, reject) { + var promise; + + if(notCall) { + promise = instance.balanceOf(address) + .then(function() { + return instance.balanceOf.call(address); + }); + } + else { + promise = instance.balanceOf.call(address); + } + + promise.then(function(balance) { + try { + assert.equal(balance.valueOf(), expectedBalance, "balance is not equal"); + } + catch(err) { + reject(err); + + return; + } + + resolve(); + }); + }); +} + +function getDividend(instance, id) { + return instance.dividends.call(id) + .then(function(obj) { + return { + id: obj[0].valueOf(), + block: obj[1].valueOf(), + time: obj[2].valueOf(), + amount: obj[3].valueOf(), + + claimedAmount: obj[4].valueOf(), + transferedBack: obj[5].valueOf(), + + totalSupply: obj[6].valueOf(), + recycleTime: obj[7].valueOf(), + + recycled: obj[8], + + claimed: obj[9] + } + }); +} + +function checkDividend(dividend, id, amount, claimedAmount, transferedBack, totalSupply, recycleTime, recycled) { + return new Promise(function(resolve, reject) { + try { + assert.equal(dividend.id, id, "dividend id is not equal"); + assert.equal(dividend.amount, amount, "dividend amount id is not equal"); + assert.equal(dividend.claimedAmount, claimedAmount, "dividend claimed amount is not equal"); + assert.equal(dividend.transferedBack, transferedBack, "dividend transfered back is not equal"); + assert.equal(dividend.totalSupply, totalSupply, "dividend total supply is not equal"); + assert.equal(dividend.recycleTime, recycleTime, "dividend recycle time is not equal"); + assert.equal(dividend.recycled, recycled, "dividend recycled is not equal"); + + resolve(); + } + catch(err) { + reject(err); + } + }); +} + +function getEmission(instance, id) { + "use strict"; + + return instance.emissions.call(id) + .then(function(obj) { + return { + blockDuration: obj[0].valueOf(), + blockTokens: obj[1].valueOf(), + periodEndsAt: obj[2].valueOf(), + removed: obj[3].valueOf() + } + }); +} + +function checkEmission(emission, blockDuration, blockTokens, periodEndsAt, removed) { + "use strict"; + + return new Promise(function(resolve, reject) { + try { + assert.equal(emission.blockDuration, blockDuration, "emission blockDuration is not equal"); + assert.equal(emission.blockTokens, blockTokens, "emission blockTokens is not equal"); + assert.equal(emission.periodEndsAt, periodEndsAt, "emission periodEndsAt is not equal"); + assert.equal(emission.removed, removed, "emission removed is not equal"); + + resolve(); + } + catch(err) { + reject(err); + } + }); +} + +function checkClaimedTokensAmount(instance, offsetDate, lastClaimedAt, currentTime, currentBalance, totalSupply, expectedValue) { + return instance.calculateEmissionTokens(offsetDate + lastClaimedAt, offsetDate + currentTime, currentBalance, totalSupply) + .then(function() { + return instance.calculateEmissionTokens.call(offsetDate + lastClaimedAt, offsetDate + currentTime, currentBalance, totalSupply); + }) + .then(function(result) { + assert.equal(result.valueOf(), expectedValue.valueOf(), "amount is not equal"); + }); +} + +function getPhase(instance, id) { + return instance.phases.call(id) + .then(function(obj) { + if(obj.length == 3) { + return { + priceShouldMultiply: obj[0].valueOf(), + price: obj[1].valueOf(), + maxAmount: obj[2].valueOf(), + } + } + + return { + price: obj[0].valueOf(), + maxAmount: obj[1].valueOf(), + } + }); +} + +function checkPhase(phase, price, maxAmount) { + return new Promise(function(resolve, reject) { + try { + assert.equal(phase.price, price, "phase price is not equal"); + assert.equal(phase.maxAmount, maxAmount, "phase maxAmount is not equal"); + + resolve(); + } + catch(err) { + reject(err); + } + }); +} + +function timeout(timeout) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + resolve(); + }, timeout * 1000); + }) +} + +function getEtherBalance(_address) { + return web3.eth.getBalance(_address); +} + +function checkEtherBalance(_address, expectedBalance) { + var balance = web3.eth.getBalance(_address); + + assert.equal(balance.valueOf(), expectedBalance.valueOf(), "address balance is not equal"); +} + +function getTxCost(result) { + var tx = web3.eth.getTransaction(result.tx); + + return result.receipt.gasUsed * tx.gasPrice; +} + +async function timeJump(seconds) { + return new Promise(function(resolve, reject) { + web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [ seconds ], + id: new Date().getTime(), + }, + function(error) { + if (error) { + return reject(error); + } + + web3.currentProvider.sendAsync( + { + jsonrpc: "2.0", + method: "evm_mine", + params: [], + id: new Date().getTime(), + }, + (err2) => { + if (err2) return reject(err2); + resolve(); + }, + ); + }, + ); + }); +} + +async function sendTransaction(contract, from, value, data, shouldFail) { + try { + await contract.sendTransaction({ from, value, data }); + + if(shouldFail) { + assert.fail("sendTransaction succeed"); + } + } + catch(err) { + if(err.constructor.name == "AssertionError") { + throw err; + } + + if(!shouldFail) { + throw err; + } + } +} + +async function transferERC20(contract, from, to, tokens, shouldFail) { + try { + await contract.transfer(to, tokens, { from }); + + if(shouldFail) { + assert.fail("erc20 transfer succeed"); + } + } + catch(err) { + if(err.constructor.name == "AssertionError") { + throw err; + } + + if(!shouldFail) { + throw err; + } + } +} + +async function balanceERC20(contract, holder, balance) { + assert.equal(await contract.balanceOf(holder), balance, "erc20 balance is not equal"); +} + +async function testTransferFrom(contract, granter, grantee, to, tokens) { + const granterBeforeBalance = await contract.balanceOf(granter); + const granteeBeforeBalance = await contract.balanceOf(grantee); + + await contract.approve(grantee, tokens, { from: granter }); + + // try to transfer more than allowed + + let trasnferFromResponse = await contract.transferFrom.call(granter, to, tokens + 1, {from: grantee}); + + assert.equal(trasnferFromResponse.valueOf(), false, "transferFrom with exceding amount succeed"); + + await contract.transferFrom(granter, to, tokens + 1, { from: grantee }); + + await checkState({ contract }, { + contract: { + balanceOf: [ + {[granter]: granterBeforeBalance}, + {[grantee]: granteeBeforeBalance} + ], + allowance: { + __val: tokens, + _owner: granter, + _spender: grantee + } + } + }); + + // try to transfer less than allowed + transferFromResponse = await contract.transferFrom.call(granter, to, tokens / 2, { from: grantee }); + + assert.equal(transferFromResponse, true, "transferFrom with lower amount failed"); + await contract.transferFrom(granter, to, tokens / 2, { from: grantee }); + + await checkState({ contract }, { + contract: { + balanceOf: [ + { [granter]: new BN(granterBeforeBalance).sub(new BN(tokens).div(2)).valueOf() }, + { [to]: new BN(granteeBeforeBalance).add(new BN(tokens).div(2)).valueOf() } + ], + + allowance: { + __val: new BN(tokens).sub(new BN(tokens).div(2)).valueOf(), + _owner: granter, + _spender: grantee + } + } + }); +} + +async function checkStateMethod(contract, contractId, stateId, args) { + if(Array.isArray(args)) { + for(let item of args) { + await checkStateMethod(contract, contractId, stateId, item); + } + } + else if(typeof args == "object" && args.constructor.name != "BN") { + const keys = Object.keys(args); + + if(keys.length == 1) { + const val = (await contract[stateId].call(keys[0])).valueOf(); + + assert.equal(val, args[keys[0]], + `Contract ${contractId} state ${stateId} with arg ${keys[0]} & value ${val} is not equal to ${args[keys[0]]}`); + + return; + } + + const passArgs = []; + + if(! args.hasOwnProperty("__val")) { + assert.fail(new Error("__val is not present")); + } + + for(let arg of Object.keys(args)) { + if(arg == "__val") { + continue; + } + + passArgs.push(args[arg]); + } + + const val = (await contract[stateId].call( ...passArgs )).valueOf(); + + assert.equal(val, args["__val"], `Contract ${contractId} state ${stateId} with value ${val} is not equal to ${args['__val']}`); + } + else { + const val = (await contract[stateId].call()).valueOf(); + + assert.equal(val, args, `Contract ${contractId} state ${stateId} with value ${val} is not equal to ${args.valueOf()}`); + } +} + +async function checkState(contracts, states) { + for(let contractId in states) { + if(! contracts.hasOwnProperty(contractId)) { + assert.fail("no such contract " + contractId); + } + + let contract = contracts[contractId]; + + for(let stateId in states[contractId]) { + if(! contract.hasOwnProperty(stateId)) { + assert.fail("no such property " + stateId); + } + + await checkStateMethod(contract, contractId, stateId, states[contractId][stateId]); + } + } +} + +async function shouldFail(call) { + try { + await call; + + assert.fail("call succeed"); + } + catch(err) { + if(err.constructor.name == "AssertionError") { + throw err; + } + } +} + +module.exports = { + receiptShouldSucceed: receiptShouldSucceed, + receiptShouldFailed: receiptShouldFailed, + receiptShouldSucceedS: receiptShouldSucceedS, + receiptShouldFailedS: receiptShouldFailedS, + catchReceiptShouldFailed: catchReceiptShouldFailed, + balanceShouldEqualTo: balanceShouldEqualTo, + getDividend: getDividend, + checkDividend: checkDividend, + getPhase: getPhase, + checkPhase: checkPhase, + getEmission: getEmission, + checkEmission: checkEmission, + checkClaimedTokensAmount: checkClaimedTokensAmount, + timeout: timeout, + getEtherBalance: getEtherBalance, + checkEtherBalance: checkEtherBalance, + getTxCost: getTxCost, + timeJump: timeJump, + + sendTransaction: sendTransaction, + + checkState: checkState, + + shouldFail: shouldFail, + + // erc20 + erc20: { + transfer: transferERC20, + balanceShouldEqualTo: balanceERC20, + test: { + transferFrom: testTransferFrom + } + }, +}; \ No newline at end of file diff --git a/test/voting.test.js b/test/voting.test.js index 47a2cf3..425c6a5 100644 --- a/test/voting.test.js +++ b/test/voting.test.js @@ -52,7 +52,8 @@ contract('Voting', async (accounts) => { blockReward.address, voting.address ) - + let ballotLimitPerValidator = (await voting.getBallotLimitPerValidator()).toNumber() + ballotLimitPerValidator.should.be.equal(Math.floor(100)) await consensus.setNewValidatorSetMock(validators) await consensus.setFinalizedMock(false, {from: owner}) await consensus.setSystemAddressMock(owner, {from: owner}) @@ -146,6 +147,7 @@ contract('Voting', async (accounts) => { it('should fail if creating ballot over the ballots limit', async () => { let maxLimitOfBallots = (await voting.MAX_LIMIT_OF_BALLOTS()).toNumber() let validatorsCount = (await consensus.currentValidatorsLength()).toNumber() + console.log({validatorsCount}); let ballotLimitPerValidator = (await voting.getBallotLimitPerValidator()).toNumber() ballotLimitPerValidator.should.be.equal(Math.floor(maxLimitOfBallots / validatorsCount)) // create ballots successfully up to the limit @@ -159,6 +161,17 @@ contract('Voting', async (accounts) => { // create a ballot with different key successfully await voting.newBallot(voteStartAfterNumberOfCycles, voteCyclesDuration, contractType, proposedValue, 'description', {from: validators[1]}).should.be.fulfilled }) + it(' getBallotLimitPerValidator should return 1 if currentValidatorsLength > MAX_LIMIT_OF_BALLOTS', async () => { + await consensus.setNewValidatorSetMock(accounts) + await consensus.setFinalizedMock(false, {from: owner}) + await consensus.setSystemAddressMock(owner, {from: owner}) + await consensus.finalizeChange().should.be.fulfilled + let maxLimitOfBallots = (await voting.MAX_LIMIT_OF_BALLOTS()).toNumber() + let validatorsCount = (await consensus.currentValidatorsLength()).toNumber() + console.log({validatorsCount}); + let ballotLimitPerValidator = (await voting.getBallotLimitPerValidator()).toNumber() + ballotLimitPerValidator.should.be.equal(1) + }) }) describe('vote', async () => { @@ -326,7 +339,7 @@ contract('Voting', async (accounts) => { let currentBlock = toBN(await web3.eth.getBlockNumber()) let voteStartBlock = await voting.getStartBlock(firstBallotId) let blocksToAdvance = voteStartBlock.sub(currentBlock) - await advanceBlocks(blocksToAdvance.toNumber()) + await advanceBlocks(blocksToAdvance.toNumber() + 1) true.should.be.equal(await voting.isActiveBallot(firstBallotId)) true.should.be.equal(await voting.isActiveBallot(secondBallotId)) false.should.be.equal(await voting.isActiveBallot(thirdBallotId)) @@ -417,7 +430,7 @@ contract('Voting', async (accounts) => { // advance until 3rd ballot is open currentBlock = toBN(await web3.eth.getBlockNumber()) voteStartBlock = await voting.getStartBlock(thirdBallotId) - await advanceBlocks(voteStartBlock.sub(currentBlock).toNumber()) + await advanceBlocks(voteStartBlock.sub(currentBlock).toNumber() + 1) true.should.be.equal(await voting.isActiveBallot(firstBallotId)) true.should.be.equal(await voting.isActiveBallot(secondBallotId)) true.should.be.equal(await voting.isActiveBallot(thirdBallotId)) @@ -692,4 +705,5 @@ contract('Voting', async (accounts) => { toBN(newValue).should.be.bignumber.equal(await votingNew.getNextBallotId()) }) }) + }) diff --git a/truffle-config.js b/truffle-config.js index 7f35e9e..b753ccf 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -36,6 +36,7 @@ module.exports = { network_id: '*', gas: 10000000 }, + fuse: { provider: walletProvider, network_id: 122, @@ -58,6 +59,7 @@ module.exports = { } } }, + plugins: ["solidity-coverage"], mocha: { reporter: 'eth-gas-reporter', reporterOptions: {