From 19ad136d7ec64c14c9b78015dd5652a7301190db Mon Sep 17 00:00:00 2001 From: Wyatt Barnes Date: Mon, 9 Mar 2020 12:21:32 -0500 Subject: [PATCH] Contracts Refactor (#149) * format * resolve conflicts * Init test for Safe constructor * Init test for releaseErc method * Init releaseNFT test * Refactor name of variables. Assign magic numbers to named variables for clarity * Update DepositCounts to public * Remove unused validator variables * Rename variables and replace magic numbers with variables for clarity. Add depositID test * Rename variables and replace magic numbers with variables for clarity. Add depositId test * Replace magic address with variable for clarity * Rename tokenId to tokenID for consistency * Remove abi dir * Add truffle-assertions package to test events in EVM smart contracts * Update path to ignore contracts dir to be only dir in root * Add Bridge contract that's a refactored verison of emitter * Add refactored version of Safe.sol * Init Validator contract * Inint ERC20Handler * Init ERC20Handler and Validator interfaces * Temporarily whitelist path to evm Bridge contract tests * Init depositERC20 test * Add support for Generic and ERC721 deposits * Add carrot to solidity version to allow for minor upgrades * Update ganache-cli and truffle packages to latest version * Replace usage of ERC20 contract with IERC20. Add releaseERC20 method * Remove ERC20Safe constructor call. Add withdrawERC20 method * Remove executeDeposit. Add withdrawERC20 * Add ValidatorActionType, ThresholdType, and Vote enums. Add hasVoted, createValidatorProposal, voteValidatorProposal, createThresholdProposal, and voteThresholdProposal function signatures * Init ERC721Safe, ERC721Handler, IDepositHandler, IERC721Handler * Formatting of test message * Add ValidatorActionType, ThresholdType, VoteStatus enums. Add createThresholdProposal and voteThresholdProposal function signatures * Add createValidatorProposal * Add hasVoted. Formatting and spelling correction * Spelling * Rename validatorAddress to proposedAddress. Add createValidatorThresholdProposal and voteValidatorThresholdProposal * Add createValidatorThresholdProposal and voteValidatorThresholdProposal * Fix type in voteValidatorThresholdProposal * Add createValidatorThresholdProposal method * Add voteValidatorThresholdProposal method * Add getDepositCount, getGenericDepositRecord, getERC20DepositRecord, getERC721DepositRecord, getDepositProposal methods * Add getValidatorThreshold and getCurrentValidatorThresholdProposal methods * Add getValidatorThreshold, getCurrentValidatorThresholdProposal, getValidatorProposal, getValidatorThreshold, getCurrentValidatorThresholdProposal, getValidatorProposal methods * Add comments * Add ValidatorAdded, ValidatorRemoved, and ValidatorThresholdChanged events * Add ValidatorThresholdChanged event * Add ValidatorProposalCreated and ValidatorProposalVote events * Add ValidatorThresholdProposalCreated and ValidatorThresholdProposalVote events * Add Validator contract * Formatting * Remove old Erc20 and Erc721 handlers * Change _depositCount to be incremented before assignment as depositID * Add extra data parsing assembly from old Erc721 handler * Additonal test to complete ERC20 deposit coverage * Init test file * Init test file * Add initialValidatorThreshold to constructor * Add _addValidator and _removeValidator. Add constructor with initialValidators and initialValidatorThreshold * Init test file * Rename test describe string to provide better context * Init test file * renamed gitattributes (#164) * Rename bridge test dir to chainBridge to avoid name conflicts * Update usage of decimals in assembly block to hexadecimal for consistency * Update usage of decimals in assembly block to hexadecimal for consistency. Correct copy calldata hexadecimal Co-authored-by: Gregory Markou Co-authored-by: Gregory Markou <16929357+GregTheGreek@users.noreply.github.com> Co-authored-by: Stephanie --- .gitattribute => .gitattributes | 0 .gitignore | 2 +- on-chain/evm-contracts/contracts/Bridge.sol | 387 +++++++++++ .../evm-contracts/contracts/BridgeAsset.sol | 2 +- .../evm-contracts/contracts/ERC20Safe.sol | 25 + .../evm-contracts/contracts/ERC721Safe.sol | 25 + on-chain/evm-contracts/contracts/Emitter.sol | 2 +- on-chain/evm-contracts/contracts/Receiver.sol | 2 +- on-chain/evm-contracts/contracts/Safe.sol | 2 +- .../evm-contracts/contracts/Validator.sol | 210 ++++++ .../contracts/handlers/ERC20Handler.sol | 42 ++ .../contracts/handlers/ERC721Handler.sol | 51 ++ .../contracts/handlers/Erc20Handler.sol | 42 -- .../contracts/handlers/Erc721Handler.sol | 76 --- .../contracts/interfaces/IDepositHandler.sol | 5 + .../contracts/interfaces/IERC20Handler.sol | 6 + .../contracts/interfaces/IERC721Handler.sol | 6 + .../contracts/interfaces/IHandler.sol | 2 +- .../contracts/interfaces/ISafe.sol | 2 +- .../contracts/interfaces/IValidator.sol | 20 + .../contracts/tests/SimpleEmitter.sol | 2 +- on-chain/evm-contracts/package-lock.json | 645 ++++++++---------- on-chain/evm-contracts/package.json | 5 +- .../test/chainBridge/createDepositProposal.js | 388 +++++++++++ .../test/chainBridge/depositERC20.js | 181 +++++ .../test/chainBridge/depositERC721.js | 197 ++++++ .../test/chainBridge/depositGeneric.js | 212 ++++++ .../test/chainBridge/voteDepositProposal.js | 298 ++++++++ 28 files changed, 2353 insertions(+), 484 deletions(-) rename .gitattribute => .gitattributes (100%) create mode 100644 on-chain/evm-contracts/contracts/Bridge.sol create mode 100644 on-chain/evm-contracts/contracts/ERC20Safe.sol create mode 100644 on-chain/evm-contracts/contracts/ERC721Safe.sol create mode 100644 on-chain/evm-contracts/contracts/Validator.sol create mode 100644 on-chain/evm-contracts/contracts/handlers/ERC20Handler.sol create mode 100644 on-chain/evm-contracts/contracts/handlers/ERC721Handler.sol delete mode 100644 on-chain/evm-contracts/contracts/handlers/Erc20Handler.sol delete mode 100644 on-chain/evm-contracts/contracts/handlers/Erc721Handler.sol create mode 100644 on-chain/evm-contracts/contracts/interfaces/IDepositHandler.sol create mode 100644 on-chain/evm-contracts/contracts/interfaces/IERC20Handler.sol create mode 100644 on-chain/evm-contracts/contracts/interfaces/IERC721Handler.sol create mode 100644 on-chain/evm-contracts/contracts/interfaces/IValidator.sol create mode 100644 on-chain/evm-contracts/test/chainBridge/createDepositProposal.js create mode 100644 on-chain/evm-contracts/test/chainBridge/depositERC20.js create mode 100644 on-chain/evm-contracts/test/chainBridge/depositERC721.js create mode 100644 on-chain/evm-contracts/test/chainBridge/depositGeneric.js create mode 100644 on-chain/evm-contracts/test/chainBridge/voteDepositProposal.js diff --git a/.gitattribute b/.gitattributes similarity index 100% rename from .gitattribute rename to .gitattributes diff --git a/.gitignore b/.gitignore index fd723654b..a2269bc8c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,5 @@ node_modules/ centrifuge-chain/ -contracts/ +./contracts/ abi/ diff --git a/on-chain/evm-contracts/contracts/Bridge.sol b/on-chain/evm-contracts/contracts/Bridge.sol new file mode 100644 index 000000000..ecd4d8745 --- /dev/null +++ b/on-chain/evm-contracts/contracts/Bridge.sol @@ -0,0 +1,387 @@ +pragma solidity ^0.5.12; + +import "./helpers/SafeMath.sol"; +import "./interfaces/IValidator.sol"; +import "./interfaces/IERC20Handler.sol"; +import "./interfaces/IERC721Handler.sol"; +import "./interfaces/IDepositHandler.sol"; + +contract Bridge { + using SafeMath for uint; + + IValidator public _validatorContract; + uint public _validatorThreshold; + ValidatorThresholdProposal public _currentValidatorThresholdProposal; + + enum Vote {No, Yes} + + // ValidatorThresholdProposalStatus and _validatorThresholdProposalStatusStrings must be kept + // the same length and order to function properly + enum ValidatorThresholdProposalStatus {Inactive, Active} + string[] _validatorThresholdProposalStatusStrings = ["inactive", "active"]; + + // DepositProposalStatus and _depositProposalStatusStrings must be kept + // the same length and order to function properly + enum DepositProposalStatus {Inactive, Active, Denied, Passed, Transferred} + string[] _depositProposalStatusStrings = ["inactive", "active", "denied", "passed", "transferred"]; + + struct GenericDepositRecord { + address _originChainTokenAddress; + address _originChainHandlerAddress; + uint _destinationChainID; + address _destinationChainHandlerAddress; + address _destinationRecipientAddress; + bytes _data; + } + + struct ERC20DepositRecord { + address _originChainTokenAddress; + address _originChainHandlerAddress; + uint _destinationChainID; + address _destinationChainHandlerAddress; + address _destinationRecipientAddress; + uint _amount; + } + + struct ERC721DepositRecord { + address _originChainTokenAddress; + address _originChainHandlerAddress; + uint _destinationChainID; + address _destinationChainHandlerAddress; + address _destinationRecipientAddress; + uint _tokenID; + bytes _data; + } + + struct DepositProposal { + uint _originChainID; + uint _depositID; + bytes32 _dataHash; + mapping(address => bool) _votes; + uint _numYes; + uint _numNo; + DepositProposalStatus _status; + } + + struct ValidatorThresholdProposal { + uint _proposedValue; + mapping(address => bool) _votes; + uint _numYes; + uint _numNo; + ValidatorThresholdProposalStatus _status; + } + + // chainID => number of deposits + mapping(uint => uint) public _depositCounts; + // chainID => depositID => GenericDepositRecord + mapping(uint => mapping(uint => GenericDepositRecord)) public _genericDepositRecords; + // chainID => depositID => ERC20DepositRecord + mapping(uint => mapping(uint => ERC20DepositRecord)) public _erc20DepositRecords; + // chainID => depositID => ERC721DepositRecord + mapping(uint => mapping(uint => ERC721DepositRecord)) public _erc721DepositRecords; + // ChainId => DepositID => Proposal + mapping(uint => mapping(uint => DepositProposal)) public _depositProposals; + + event GenericDeposited(uint indexed depositID); + event ERC20Deposited(uint indexed depositID); + event ERC721Deposited(uint indexed depositID); + event DepositProposalCreated(uint indexed originChainID, uint indexed depositID, bytes32 indexed dataHash); + event DepositProposalVote(uint indexed originChainID, uint indexed depositID, Vote indexed vote, DepositProposalStatus status); + event DepositProposalFinalized(uint indexed originChainID, uint indexed depositID); + event ValidatorThresholdProposalCreated(uint indexed proposedValue); + event ValidatorThresholdProposalVote(Vote vote); + event ValidatorThresholdChanged(uint indexed newThreshold); + + modifier _onlyValidators() { + IValidator validatorContract = IValidator(_validatorContract); + require(validatorContract.isValidator(msg.sender)); + _; + } + + constructor (address validatorContract, uint initialValidatorThreshold) public { + _validatorContract = IValidator(validatorContract); + _validatorThreshold = initialValidatorThreshold; + } + + function getValidatorThreshold() public view returns (uint) { + return _validatorThreshold; + } + + function getDepositCount(uint originChainID) public view returns (uint) { + return _depositCounts[originChainID]; + } + + function getGenericDepositRecord(uint originChainID, uint depositID) public view returns ( + address, address, uint, address, address, bytes memory) { + GenericDepositRecord memory genericDepositRecord = _genericDepositRecords[originChainID][depositID]; + return ( + genericDepositRecord._originChainTokenAddress, + genericDepositRecord._originChainHandlerAddress, + genericDepositRecord._destinationChainID, + genericDepositRecord._destinationChainHandlerAddress, + genericDepositRecord._destinationRecipientAddress, + genericDepositRecord._data); + } + + function getERC20DepositRecord(uint originChainID, uint depositID) public view returns ( + address, address, uint, address, address, uint) { + ERC20DepositRecord memory erc20DepositRecord = _erc20DepositRecords[originChainID][depositID]; + return ( + erc20DepositRecord._originChainTokenAddress, + erc20DepositRecord._originChainHandlerAddress, + erc20DepositRecord._destinationChainID, + erc20DepositRecord._destinationChainHandlerAddress, + erc20DepositRecord._destinationRecipientAddress, + erc20DepositRecord._amount); + } + + function getERC721DepositRecord(uint originChainID, uint depositID) public view returns ( + address, address, uint, address, address, uint, bytes memory) { + ERC721DepositRecord memory erc721DepositRecord = _erc721DepositRecords[originChainID][depositID]; + return ( + erc721DepositRecord._originChainTokenAddress, + erc721DepositRecord._originChainHandlerAddress, + erc721DepositRecord._destinationChainID, + erc721DepositRecord._destinationChainHandlerAddress, + erc721DepositRecord._destinationRecipientAddress, + erc721DepositRecord._tokenID, + erc721DepositRecord._data); + } + + function getCurrentValidatorThresholdProposal() public view returns ( + uint, uint, uint, string memory) { + return ( + _currentValidatorThresholdProposal._proposedValue, + _currentValidatorThresholdProposal._numYes, + _currentValidatorThresholdProposal._numNo, + _validatorThresholdProposalStatusStrings[uint(_currentValidatorThresholdProposal._status)]); + } + + function getDepositProposal(uint originChainID, uint depositID) public view returns ( + uint, uint, bytes32, uint, uint, string memory) { + DepositProposal memory depositProposal = _depositProposals[originChainID][depositID]; + return ( + depositProposal._originChainID, + depositProposal._depositID, + depositProposal._dataHash, + depositProposal._numYes, + depositProposal._numNo, + _depositProposalStatusStrings[uint(depositProposal._status)]); + } + + function hasVoted(uint originChainID, uint depositID, address validatorAddress) public view returns (bool) { + return _depositProposals[originChainID][depositID]._votes[validatorAddress]; + } + + function depositGeneric( + uint destinationChainID, + address destinationRecipientAddress, + bytes memory data + ) public { + uint depositID = ++_depositCounts[destinationChainID]; + + _genericDepositRecords[destinationChainID][depositID] = GenericDepositRecord( + address(0), + address(0), + destinationChainID, + address(0), + destinationRecipientAddress, + data + ); + + emit GenericDeposited(depositID); + } + + function depositGeneric( + address originChainContractAddress, + address originChainHandlerAddress, + uint destinationChainID, + address destinationChainHandlerAddress, + address destinationRecipientAddress, + bytes memory data + ) public { + uint depositID = ++_depositCounts[destinationChainID]; + + _genericDepositRecords[destinationChainID][depositID] = GenericDepositRecord( + originChainContractAddress, + originChainHandlerAddress, + destinationChainID, + destinationChainHandlerAddress, + destinationRecipientAddress, + data + ); + + emit GenericDeposited(depositID); + } + + function depositERC20( + address originChainTokenAddress, + address originChainHandlerAddress, + uint destinationChainID, + address destinationChainHandlerAddress, + address destinationRecipientAddress, + uint amount + ) public { + IERC20Handler erc20Handler = IERC20Handler(originChainHandlerAddress); + erc20Handler.depositERC20(originChainTokenAddress, msg.sender, amount); + + uint depositID = ++_depositCounts[destinationChainID]; + + _erc20DepositRecords[destinationChainID][depositID] = ERC20DepositRecord( + originChainTokenAddress, + originChainHandlerAddress, + destinationChainID, + destinationChainHandlerAddress, + destinationRecipientAddress, + amount + ); + + emit ERC20Deposited(depositID); + } + + function depositERC721( + address originChainTokenAddress, + address originChainHandlerAddress, + uint destinationChainID, + address destinationChainHandlerAddress, + address destinationRecipientAddress, + uint tokenID, + bytes memory data + ) public { + IERC721Handler erc721Handler = IERC721Handler(originChainHandlerAddress); + erc721Handler.depositERC721(originChainTokenAddress, msg.sender, tokenID); + + uint depositID = ++_depositCounts[destinationChainID]; + + _erc721DepositRecords[destinationChainID][depositID] = ERC721DepositRecord( + originChainTokenAddress, + originChainHandlerAddress, + destinationChainID, + destinationChainHandlerAddress, + destinationRecipientAddress, + tokenID, + data + ); + + emit ERC721Deposited(depositID); + } + + function createDepositProposal(uint originChainID, uint depositID, bytes32 dataHash) public _onlyValidators { + require(_depositProposals[originChainID][depositID]._status == DepositProposalStatus.Inactive || + _depositProposals[originChainID][depositID]._status == DepositProposalStatus.Denied, "this proposal is either currently active or has already been passed/transferred"); + + // If _depositThreshold is set to 1, then auto finalize + if (_validatorThreshold <= 1) { + _depositProposals[originChainID][depositID] = DepositProposal({ + _originChainID: originChainID, + _depositID: depositID, + _dataHash: dataHash, + _numYes: 1, // Creator always votes in favour + _numNo: 0, + _status: DepositProposalStatus.Passed + }); + } else { + _depositProposals[originChainID][depositID] = DepositProposal({ + _originChainID: originChainID, + _depositID: depositID, + _dataHash: dataHash, + _numYes: 1, // Creator always votes in favour + _numNo: 0, + _status: DepositProposalStatus.Active + }); + } + + // Creator always votes in favour + _depositProposals[originChainID][depositID]._votes[msg.sender] = true; + + emit DepositProposalCreated(originChainID, depositID, dataHash); + } + + function voteDepositProposal(uint originChainID, uint depositID, Vote vote) public _onlyValidators { + DepositProposal storage depositProposal = _depositProposals[originChainID][depositID]; + + require(depositProposal._status != DepositProposalStatus.Inactive, "proposal is not active"); + require(depositProposal._status == DepositProposalStatus.Active, "proposal has been finalized"); + require(!depositProposal._votes[msg.sender], "validator has already voted"); + require(uint(vote) <= 1, "invalid vote"); + + if (vote == Vote.Yes) { + depositProposal._numYes++; + } else { + depositProposal._numNo++; + } + + depositProposal._votes[msg.sender] = true; + + // Todo: Edge case if validator threshold changes? + if (depositProposal._numYes >= _validatorThreshold) { + depositProposal._status = DepositProposalStatus.Passed; + emit DepositProposalFinalized(originChainID, depositID); + } else if (_validatorContract.getTotalValidators().sub(depositProposal._numNo) < _validatorThreshold) { + depositProposal._status = DepositProposalStatus.Denied; + emit DepositProposalFinalized(originChainID, depositID); + } + + emit DepositProposalVote(originChainID, depositID, vote, depositProposal._status); + } + + function executeDepositProposal(uint originChainID, uint depositID, address destinationChainHandlerAddress, bytes memory data) public { + DepositProposal storage depositProposal = _depositProposals[originChainID][depositID]; + + require(depositProposal._status != DepositProposalStatus.Inactive, "proposal is not active"); + require(depositProposal._status == DepositProposalStatus.Passed, "proposal was not passed or has already been transferred"); + require(keccak256(data) == depositProposal._dataHash, "provided data does not match proposal's data hash"); + + IDepositHandler depositHandler = IDepositHandler(destinationChainHandlerAddress); + depositHandler.executeDeposit(data); + + depositProposal._status = DepositProposalStatus.Transferred; + } + + function createValidatorThresholdProposal(uint proposedValue) public _onlyValidators { + require(_currentValidatorThresholdProposal._status == ValidatorThresholdProposalStatus.Inactive, "a proposal is currently active"); + require(proposedValue <= _validatorContract.getTotalValidators(), "proposed value cannot be greater than the total number of validators"); + + _currentValidatorThresholdProposal = ValidatorThresholdProposal({ + _proposedValue: proposedValue, + _numYes: 1, // Creator always votes in favour + _numNo: 0, + _status: ValidatorThresholdProposalStatus.Active + }); + + if (_validatorThreshold <= 1) { + _validatorThreshold = _currentValidatorThresholdProposal._proposedValue; + _currentValidatorThresholdProposal._status = ValidatorThresholdProposalStatus.Inactive; + emit ValidatorThresholdChanged(proposedValue); + } + // Record vote + _currentValidatorThresholdProposal._votes[msg.sender] = true; + emit ValidatorThresholdProposalCreated(proposedValue); + } + + function voteValidatorThresholdProposal(Vote vote) public _onlyValidators { + require(_currentValidatorThresholdProposal._status == ValidatorThresholdProposalStatus.Active, "no proposal is currently active"); + require(!_currentValidatorThresholdProposal._votes[msg.sender], "validator has already voted"); + require(uint(vote) <= 1, "vote out of the vote enum range"); + + // Cast vote + if (vote == Vote.Yes) { + _currentValidatorThresholdProposal._numYes++; + } else { + _currentValidatorThresholdProposal._numNo++; + } + + _currentValidatorThresholdProposal._votes[msg.sender] = true; + emit ValidatorThresholdProposalVote(vote); + + // Todo: Edge case if validator threshold changes? + // Todo: For a proposal to pass does the number of yes votes just need to be higher than the threshold, or does it also have to be greater than the number of no votes? + if (_currentValidatorThresholdProposal._numYes >= _validatorThreshold) { + _validatorThreshold = _currentValidatorThresholdProposal._proposedValue; + _currentValidatorThresholdProposal._status = ValidatorThresholdProposalStatus.Inactive; + emit ValidatorThresholdChanged(_currentValidatorThresholdProposal._proposedValue); + } else if (_validatorContract.getTotalValidators().sub(_currentValidatorThresholdProposal._numNo) < _validatorThreshold) { + _currentValidatorThresholdProposal._status = ValidatorThresholdProposalStatus.Inactive; + } + } +} \ No newline at end of file diff --git a/on-chain/evm-contracts/contracts/BridgeAsset.sol b/on-chain/evm-contracts/contracts/BridgeAsset.sol index 2036fa87d..8b877ae6d 100644 --- a/on-chain/evm-contracts/contracts/BridgeAsset.sol +++ b/on-chain/evm-contracts/contracts/BridgeAsset.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; pragma experimental ABIEncoderV2; contract BridgeAsset { diff --git a/on-chain/evm-contracts/contracts/ERC20Safe.sol b/on-chain/evm-contracts/contracts/ERC20Safe.sol new file mode 100644 index 000000000..858a2786b --- /dev/null +++ b/on-chain/evm-contracts/contracts/ERC20Safe.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.5.12; + +import "./helpers/SafeMath.sol"; +import "./erc/ERC20/IERC20.sol"; + +contract ERC20Safe { + using SafeMath for uint; + + // ERC20 contract => amount of tokens owned by Safe + mapping(address => uint) public _balances; + + function lockERC20(address tokenAddress, address owner, address recipient, uint amount) internal { + IERC20 erc20 = IERC20(tokenAddress); + erc20.transferFrom(owner, recipient, amount); + + _balances[tokenAddress] = _balances[tokenAddress].add(amount); + } + + function releaseERC20(address tokenAddress, address owner, address recipient, uint amount) internal { + IERC20 erc20 = IERC20(tokenAddress); + erc20.transferFrom(owner, recipient, amount); + + _balances[tokenAddress] = _balances[tokenAddress].sub(amount); + } +} \ No newline at end of file diff --git a/on-chain/evm-contracts/contracts/ERC721Safe.sol b/on-chain/evm-contracts/contracts/ERC721Safe.sol new file mode 100644 index 000000000..b3d67879d --- /dev/null +++ b/on-chain/evm-contracts/contracts/ERC721Safe.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.5.12; + +import "./helpers/SafeMath.sol"; +import "./erc/ERC721/IERC721.sol"; + +contract ERC721Safe { + using SafeMath for uint; + + // ERC721 contract => amount of tokens owned by Safe + mapping(address => uint) public _balances; + + function lockERC721(address tokenAddress, address owner, address recipient, uint tokenID) internal { + IERC721 erc721 = IERC721(tokenAddress); + erc721.transferFrom(owner, recipient, tokenID); + + _balances[tokenAddress] = _balances[tokenAddress].add(1); + } + + function releaseERC721(address tokenAddress, address owner, address recipient, uint tokenID) internal { + IERC721 erc721 = IERC721(tokenAddress); + erc721.transferFrom(owner, recipient, tokenID); + + _balances[tokenAddress] = _balances[tokenAddress].sub(1); + } +} diff --git a/on-chain/evm-contracts/contracts/Emitter.sol b/on-chain/evm-contracts/contracts/Emitter.sol index 2ff58c278..9f9e60081 100644 --- a/on-chain/evm-contracts/contracts/Emitter.sol +++ b/on-chain/evm-contracts/contracts/Emitter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; import "./Safe.sol"; diff --git a/on-chain/evm-contracts/contracts/Receiver.sol b/on-chain/evm-contracts/contracts/Receiver.sol index 0b969da6d..b99b71802 100644 --- a/on-chain/evm-contracts/contracts/Receiver.sol +++ b/on-chain/evm-contracts/contracts/Receiver.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; import "./interfaces/IHandler.sol"; diff --git a/on-chain/evm-contracts/contracts/Safe.sol b/on-chain/evm-contracts/contracts/Safe.sol index d3d8234f3..797488c8b 100644 --- a/on-chain/evm-contracts/contracts/Safe.sol +++ b/on-chain/evm-contracts/contracts/Safe.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; import "./helpers/SafeMath.sol"; import "./erc/ERC20/ERC20.sol"; diff --git a/on-chain/evm-contracts/contracts/Validator.sol b/on-chain/evm-contracts/contracts/Validator.sol new file mode 100644 index 000000000..4c306ef35 --- /dev/null +++ b/on-chain/evm-contracts/contracts/Validator.sol @@ -0,0 +1,210 @@ +pragma solidity ^0.5.12; + +import "./interfaces/IValidator.sol"; +import "./helpers/SafeMath.sol"; + +contract Validator is IValidator { + using SafeMath for uint; + + uint public _validatorThreshold; + uint public _totalValidators; + ValidatorThresholdProposal private _currentValidatorThresholdProposal; + // ValidatorActionType and _validatorActionTypeStrings must be kept + // the same length and order to function properly + string[] _validatorActionTypeStrings = ["remove", "add"]; + // VoteStatus and _voteStatusStrings must be kept + // the same length and order to function properly + string[] _voteStatusStrings = ["inactive", "active"]; + + struct ValidatorProposal { + address _proposedAddress; + ValidatorActionType _action; + mapping(address => bool) _votes; + uint _numYes; + uint _numNo; + VoteStatus _status; + } + + struct ValidatorThresholdProposal { + uint _proposedValue; + mapping(address => bool) _votes; + uint _numYes; + uint _numNo; + VoteStatus _status; + } + + // Validator Address => whether they are a Validator + mapping(address => bool) public _validators; + // Validator Address => ValidatorProposal + mapping(address => ValidatorProposal) public _validatorProposals; + + event ValidatorProposalCreated(address indexed proposedAddress, ValidatorActionType indexed validatorActionType); + event ValidatorProposalVote(address indexed proposedAddress, Vote vote); + event ValidatorAdded(address indexed validatorAddress); + event ValidatorRemoved(address indexed validatorAddress); + event ValidatorThresholdProposalCreated(uint indexed proposedValue); + event ValidatorThresholdProposalVote(Vote vote); + event ValidatorThresholdChanged(uint indexed newThreshold); + + modifier _onlyValidators() { + require(_validators[msg.sender], "sender is not a validator"); + _; + } + + constructor (address[] memory initialValidators, uint initialValidatorThreshold) public { + for (uint i; i < initialValidators.length; i++) { + _addValidator(initialValidators[i]); + } + + _validatorThreshold = initialValidatorThreshold; + } + + function isValidator(address validatorAddress) public returns (bool) { + return _validators[validatorAddress]; + } + + function getValidatorThreshold() public view returns (uint) { + return _validatorThreshold; + } + + function getTotalValidators() public returns (uint) { + return _totalValidators; + } + + function getCurrentValidatorThresholdProposal() public view returns ( + uint, uint, uint, string memory) { + return ( + _currentValidatorThresholdProposal._proposedValue, + _currentValidatorThresholdProposal._numYes, + _currentValidatorThresholdProposal._numNo, + _voteStatusStrings[uint(_currentValidatorThresholdProposal._status)]); + } + + function getValidatorProposal(address proposedAddress) public view returns ( + address, string memory, uint, uint, string memory) { + ValidatorProposal memory validatorProposal = _validatorProposals[proposedAddress]; + return ( + validatorProposal._proposedAddress, + _validatorActionTypeStrings[uint(validatorProposal._action)], + validatorProposal._numYes, + validatorProposal._numNo, + _voteStatusStrings[uint(validatorProposal._status)]); + } + + function createValidatorProposal(address proposedAddress, ValidatorActionType action) public _onlyValidators { + require(uint(action) <= 1, "action out of the vote enum range"); + require(action == ValidatorActionType.Remove && _validators[proposedAddress] == true, "address is not a validator"); + require(action == ValidatorActionType.Add && _validators[proposedAddress] == false, "address is currently a validator"); + require(_validatorProposals[proposedAddress]._status == VoteStatus.Inactive, "there is already an active proposal for this address"); + + _validatorProposals[proposedAddress] = ValidatorProposal({ + _proposedAddress: proposedAddress, + _action: action, + _numYes: 1, // Creator always votes in favour + _numNo: 0, + _status: VoteStatus.Active + }); + + if (_validatorThreshold <= 1) { + _validatorProposals[proposedAddress]._status = VoteStatus.Inactive; + if (action == ValidatorActionType.Add) { + _addValidator(proposedAddress); + } else { + _removeValidator(proposedAddress); + } + } + // Record vote + _validatorProposals[proposedAddress]._votes[msg.sender] = true; + emit ValidatorProposalCreated(proposedAddress, action); + } + + function voteValidatorProposal(address proposedAddress, Vote vote) public _onlyValidators { + require(_validatorProposals[proposedAddress]._status == VoteStatus.Active, "there is no active proposal for this address"); + require(!_validatorProposals[proposedAddress]._votes[msg.sender], "validator has already voted"); + require(uint(vote) <= 1, "vote out of the vote enum range"); + + // Cast vote + if (vote == Vote.Yes) { + _validatorProposals[proposedAddress]._numYes++; + } else { + _validatorProposals[proposedAddress]._numNo++; + } + + // Record vote + _validatorProposals[proposedAddress]._votes[msg.sender] = true; + emit ValidatorProposalVote(proposedAddress, vote); + + // Todo: Edge case if validator threshold changes? + // Todo: For a proposal to pass does the number of yes votes just need to be higher than the threshold, or does it also have to be greater than the number of no votes? + if (_validatorProposals[proposedAddress]._numYes >= _validatorThreshold) { + if (_validatorProposals[proposedAddress]._action == ValidatorActionType.Add) { + _addValidator(proposedAddress); + } else { + _removeValidator(proposedAddress); + } + + _validatorProposals[proposedAddress]._status = VoteStatus.Inactive; + } else if (_totalValidators.sub(_validatorProposals[proposedAddress]._numNo) < _validatorThreshold) { + _validatorProposals[proposedAddress]._status = VoteStatus.Inactive; + } + } + + function createValidatorThresholdProposal(uint proposedValue) public _onlyValidators { + require(_currentValidatorThresholdProposal._status == VoteStatus.Inactive, "a proposal is currently active"); + require(proposedValue <= _totalValidators, "proposed value cannot be greater than the total number of validators"); + + _currentValidatorThresholdProposal = ValidatorThresholdProposal({ + _proposedValue: proposedValue, + _numYes: 1, // Creator always votes in favour + _numNo: 0, + _status: VoteStatus.Active + }); + + if (_validatorThreshold <= 1) { + _validatorThreshold = _currentValidatorThresholdProposal._proposedValue; + _currentValidatorThresholdProposal._status = VoteStatus.Inactive; + emit ValidatorThresholdChanged(proposedValue); + } + // Record vote + _currentValidatorThresholdProposal._votes[msg.sender] = true; + emit ValidatorThresholdProposalCreated(proposedValue); + } + + function voteValidatorThresholdProposal(Vote vote) public _onlyValidators { + require(_currentValidatorThresholdProposal._status == VoteStatus.Active, "no proposal is currently active"); + require(!_currentValidatorThresholdProposal._votes[msg.sender], "validator has already voted"); + require(uint(vote) <= 1, "vote out of the vote enum range"); + + // Cast vote + if (vote == Vote.Yes) { + _currentValidatorThresholdProposal._numYes++; + } else { + _currentValidatorThresholdProposal._numNo++; + } + + _currentValidatorThresholdProposal._votes[msg.sender] = true; + emit ValidatorThresholdProposalVote(vote); + + // Todo: Edge case if validator threshold changes? + // Todo: For a proposal to pass does the number of yes votes just need to be higher than the threshold, or does it also have to be greater than the number of no votes? + if (_currentValidatorThresholdProposal._numYes >= _validatorThreshold) { + _validatorThreshold = _currentValidatorThresholdProposal._proposedValue; + _currentValidatorThresholdProposal._status = VoteStatus.Inactive; + emit ValidatorThresholdChanged(_currentValidatorThresholdProposal._proposedValue); + } else if (_totalValidators.sub(_currentValidatorThresholdProposal._numNo) < _validatorThreshold) { + _currentValidatorThresholdProposal._status = VoteStatus.Inactive; + } + } + + function _addValidator(address addr) internal { + _validators[addr] = true; + _totalValidators++; + emit ValidatorAdded(addr); + } + + function _removeValidator(address addr) internal { + _validators[addr] = false; + _totalValidators--; + emit ValidatorRemoved(addr); + } +} diff --git a/on-chain/evm-contracts/contracts/handlers/ERC20Handler.sol b/on-chain/evm-contracts/contracts/handlers/ERC20Handler.sol new file mode 100644 index 000000000..b04695558 --- /dev/null +++ b/on-chain/evm-contracts/contracts/handlers/ERC20Handler.sol @@ -0,0 +1,42 @@ +pragma solidity ^0.5.12; + +import "../interfaces/IERC20Handler.sol"; +import "../ERC20Safe.sol"; +import "../erc/ERC20/ERC20Mintable.sol"; +import "../interfaces/IDepositHandler.sol"; + +contract ERC20Handler is IERC20Handler, IDepositHandler, ERC20Safe { + address public _bridgeAddress; + + modifier _onlyBridge() { + require(msg.sender == _bridgeAddress); + _; + } + + constructor(address bridgeAddress) public { + _bridgeAddress = bridgeAddress; + } + + function depositERC20(address tokenAddress, address owner, uint amount) public _onlyBridge { + lockERC20(tokenAddress, owner, address(this), amount); + } + + function executeDeposit(bytes memory data) public { + address destinationChainTokenAddress; + address destinationRecipientAddress; + uint amount; + + assembly { + destinationChainTokenAddress := mload(add(data, 0x20)) + destinationRecipientAddress := mload(add(data, 0x40)) + amount := mload(add(data, 0x60)) + } + + ERC20Mintable erc20 = ERC20Mintable(destinationChainTokenAddress); + erc20.mint(destinationRecipientAddress, amount); + } + + function withdrawERC20(address tokenAddress, address recipient, uint amount) public _onlyBridge { + releaseERC20(tokenAddress, address(this), recipient, amount); + } +} diff --git a/on-chain/evm-contracts/contracts/handlers/ERC721Handler.sol b/on-chain/evm-contracts/contracts/handlers/ERC721Handler.sol new file mode 100644 index 000000000..89403721f --- /dev/null +++ b/on-chain/evm-contracts/contracts/handlers/ERC721Handler.sol @@ -0,0 +1,51 @@ +pragma solidity ^0.5.12; + +import "../interfaces/IERC721Handler.sol"; +import "../ERC721Safe.sol"; +import "../interfaces/IDepositHandler.sol"; +import "../erc/ERC721/ERC721Mintable.sol"; + +contract ERC721Handler is IERC721Handler, IDepositHandler, ERC721Safe { + address public _bridgeAddress; + + modifier _onlyBridge() { + require(msg.sender == _bridgeAddress); + _; + } + + constructor(address bridgeAddress) public { + _bridgeAddress = bridgeAddress; + } + + function depositERC721(address tokenAddress, address owner, uint tokenID) public _onlyBridge { + lockERC721(tokenAddress, owner, address(this), tokenID); + } + + function executeDeposit(bytes memory data) public { + address destinationChainTokenAddress; + address destinationRecipientAddress; + uint tokenID; + bytes memory extraData; + + assembly { + destinationChainTokenAddress := mload(add(data, 0x20)) + destinationRecipientAddress := mload(add(data, 0x40)) + tokenID := mload(add(data, 0x60)) + extraData := mload(0x40) + let lenextra := mload(add(0x80, data)) + mstore(0x40, add(0x60, add(extraData, lenextra))) + calldatacopy( + extraData, // copy to extra + 0xA0, // copy from calldata @ 0xA0 + sub(calldatasize, 0xA0) // copy size (calldatasize - 0xA0) + ) + } + + ERC721Mintable erc721 = ERC721Mintable(destinationChainTokenAddress); + erc721.safeMint(destinationRecipientAddress, tokenID, extraData); + } + + function withdrawERC721(address tokenAddress, address recipient, uint tokenID) public _onlyBridge { + releaseERC721(tokenAddress, address(this), recipient, tokenID); + } +} diff --git a/on-chain/evm-contracts/contracts/handlers/Erc20Handler.sol b/on-chain/evm-contracts/contracts/handlers/Erc20Handler.sol deleted file mode 100644 index feee64fe2..000000000 --- a/on-chain/evm-contracts/contracts/handlers/Erc20Handler.sol +++ /dev/null @@ -1,42 +0,0 @@ -pragma solidity 0.5.12; - -import "../erc/ERC20/ERC20Mintable.sol"; -import "../helpers/Ownable.sol"; - -/** - * @dev A handler for the ERC20 token standard. - */ -contract ERC20Handler is Ownable { - // ChainId originChainAddress => currentChainAddress - mapping(uint => mapping(address => address)) public whitelist; - - // ChainId originChainAddress => currentChainAddress - mapping(uint => mapping(address => address)) public synthetics; - - function executeTransfer(uint _originChain, address _originTokenAddress, address _to, uint _value) public onlyOwner { - if (whitelist[_originChain][_originTokenAddress] != address(0)) { - // Token is whitelisted - // tokenAddress might not be needed - address tokenAddress = whitelist[_originChain][_originTokenAddress]; - ERC20Mintable token = ERC20Mintable(tokenAddress); - // Mint new tokens - token.mint(_to, _value); - } else { - if (synthetics[_originChain][_originTokenAddress] != address(0)) { - // token exists - // tokenAddress might not be needed - address tokenAddress = synthetics[_originChain][_originTokenAddress]; - ERC20Mintable token = ERC20Mintable(tokenAddress); - // Mint new contract - token.mint(_to, _value); - } else { - // Token doesn't exist - ERC20Mintable token = new ERC20Mintable(); - // Create a relationship between the originAddress and the synthetic - synthetics[_originChain][_originTokenAddress] = address(token); - // Mint new tokens - token.mint(_to, _value); - } - } - } -} \ No newline at end of file diff --git a/on-chain/evm-contracts/contracts/handlers/Erc721Handler.sol b/on-chain/evm-contracts/contracts/handlers/Erc721Handler.sol deleted file mode 100644 index 89fd90e4b..000000000 --- a/on-chain/evm-contracts/contracts/handlers/Erc721Handler.sol +++ /dev/null @@ -1,76 +0,0 @@ -pragma solidity 0.5.12; - -import "../erc/ERC721/ERC721Mintable.sol"; -import "../interfaces/IHandler.sol"; -import "../helpers/Ownable.sol"; - -/** - * @dev A handler for the ERC71 token standard. - */ -contract ERC721Handler is Ownable, IHandler { - // ChainId originChainAddress => currentChainAddress - mapping(uint => mapping(address => address)) public whitelist; - - // ChainId originChainAddress => currentChainAddress - mapping(uint => mapping(address => address)) public synthetics; - - function executeTransfer(uint _originChain, bytes memory _data) public onlyOwner { - // Decodes input bytes for _data struct - // address _originAddress @ 0x44 - // address _to @ 0x64 - // uint _value @ 0x84 - // bytes _extraData @ 0xA4 ... arbitrary length - - bytes memory _extraData; - address _to; - uint _tokenId; - address _originTokenAddress; - - - // NOTE: test using calldatacopy instead of mlad for all variables and manually setting the offset - // for the arbitrary length _extraData it might be more efficient - assembly { - // get addresses from calldata - _originTokenAddress := mload(add(0x20, _data)) - _to := mload(add(0x40, _data)) - - // get uint from calldata - _tokenId := mload(add(0x60, _data)) - - // get arbitrary length bytes from calldata - _extraData := mload(0x40) - let lenextra := mload(add(0x80, _data)) - mstore(0x40, add(0x60, add(_extraData, lenextra))) - calldatacopy( - _extraData, // copy to extra - 0xA4, // copy from calldata @ 0xA4 - sub(calldatasize, 0xA4) // copy size (calldatasize - 0xA4) - ) - } - - if (whitelist[_originChain][_originTokenAddress] != address(0)) { - // Token is whitelisted - // tokenAddress might not be needed - address tokenAddress = whitelist[_originChain][_originTokenAddress]; - ERC721Mintable token = ERC721Mintable(tokenAddress); - // Mint new tokens - token.safeMint(_to, _tokenId, _extraData); - } else { - if (synthetics[_originChain][_originTokenAddress] != address(0)) { - // token exists - // tokenAddress might not be needed - address tokenAddress = synthetics[_originChain][_originTokenAddress]; - ERC721Mintable token = ERC721Mintable(tokenAddress); - // Mint new contract - token.safeMint(_to, _tokenId, _extraData); - } else { - // Token doesn't exist - ERC721Mintable token = new ERC721Mintable(); - // Create a relationship between the originAddress and the synthetic - synthetics[_originChain][_originTokenAddress] = address(token); - // Mint new tokens - token.safeMint(_to, _tokenId, _extraData); - } - } - } -} \ No newline at end of file diff --git a/on-chain/evm-contracts/contracts/interfaces/IDepositHandler.sol b/on-chain/evm-contracts/contracts/interfaces/IDepositHandler.sol new file mode 100644 index 000000000..4eba4f1a4 --- /dev/null +++ b/on-chain/evm-contracts/contracts/interfaces/IDepositHandler.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.5.12; + +interface IDepositHandler { + function executeDeposit(bytes calldata data) external; +} \ No newline at end of file diff --git a/on-chain/evm-contracts/contracts/interfaces/IERC20Handler.sol b/on-chain/evm-contracts/contracts/interfaces/IERC20Handler.sol new file mode 100644 index 000000000..0685d6b51 --- /dev/null +++ b/on-chain/evm-contracts/contracts/interfaces/IERC20Handler.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.12; + +interface IERC20Handler { + function depositERC20(address tokenAddress, address owner, uint amount) external; + function withdrawERC20(address tokenAddress, address recipient, uint amount) external; +} diff --git a/on-chain/evm-contracts/contracts/interfaces/IERC721Handler.sol b/on-chain/evm-contracts/contracts/interfaces/IERC721Handler.sol new file mode 100644 index 000000000..35845b5d7 --- /dev/null +++ b/on-chain/evm-contracts/contracts/interfaces/IERC721Handler.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.12; + +interface IERC721Handler { + function depositERC721(address tokenAddress, address owner, uint tokenID) external; + function withdrawERC721(address tokenAddress, address recipient, uint tokenID) external; +} diff --git a/on-chain/evm-contracts/contracts/interfaces/IHandler.sol b/on-chain/evm-contracts/contracts/interfaces/IHandler.sol index ec739c7d9..ae7dc685b 100644 --- a/on-chain/evm-contracts/contracts/interfaces/IHandler.sol +++ b/on-chain/evm-contracts/contracts/interfaces/IHandler.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; import "../helpers/Ownable.sol"; diff --git a/on-chain/evm-contracts/contracts/interfaces/ISafe.sol b/on-chain/evm-contracts/contracts/interfaces/ISafe.sol index 00c4bf41e..1bba7982f 100644 --- a/on-chain/evm-contracts/contracts/interfaces/ISafe.sol +++ b/on-chain/evm-contracts/contracts/interfaces/ISafe.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; /** * @title ISafe diff --git a/on-chain/evm-contracts/contracts/interfaces/IValidator.sol b/on-chain/evm-contracts/contracts/interfaces/IValidator.sol new file mode 100644 index 000000000..0f21e549a --- /dev/null +++ b/on-chain/evm-contracts/contracts/interfaces/IValidator.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.5.12; + +interface IValidator { + enum Vote {No, Yes} + + // ValidatorActionType and _validatorActionTypeStrings must be kept + // the same length and order to function properly + enum ValidatorActionType {Remove, Add} + + // VoteStatus and _voteStatusStrings must be kept + // the same length and order to function properly + enum VoteStatus {Inactive, Active} + + function isValidator(address validatorAddress) external returns (bool); + function getTotalValidators() external returns (uint); + function createValidatorProposal(address proposedAddress, ValidatorActionType action) external; + function voteValidatorProposal(address proposedAddress, Vote vote) external; + function createValidatorThresholdProposal(uint proposedValue) external; + function voteValidatorThresholdProposal(Vote vote) external; +} diff --git a/on-chain/evm-contracts/contracts/tests/SimpleEmitter.sol b/on-chain/evm-contracts/contracts/tests/SimpleEmitter.sol index 9d5721439..6dada1156 100644 --- a/on-chain/evm-contracts/contracts/tests/SimpleEmitter.sol +++ b/on-chain/evm-contracts/contracts/tests/SimpleEmitter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.12; +pragma solidity ^0.5.12; /** * @title SimpleEmitter diff --git a/on-chain/evm-contracts/package-lock.json b/on-chain/evm-contracts/package-lock.json index e5ea78289..5fbaa764f 100644 --- a/on-chain/evm-contracts/package-lock.json +++ b/on-chain/evm-contracts/package-lock.json @@ -15,6 +15,12 @@ "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -31,7 +37,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -47,9 +53,9 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, "commander": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", - "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" }, "concat-map": { "version": "0.0.1", @@ -75,13 +81,13 @@ "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.3", - "hmac-drbg": "1.0.1", - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" } }, "escape-string-regexp": { @@ -90,13 +96,13 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "ethers": { - "version": "4.0.40", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.40.tgz", - "integrity": "sha512-MC9BtV7Hpq4dgFONEfanx9aU9GhhoWU270F+/wegHZXA7FR+2KXFdt36YIQYLmVY5ykUWswDxd+f9EVkIa7JOA==", + "version": "4.0.45", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.45.tgz", + "integrity": "sha512-N/Wmc6Mw4pQO+Sss1HnKDCSS6KSCx0luoBMiPNq+1GbOaO3YaZOyplBEhj+NEoYsizZYODtkITg2oecPeNnidQ==", "dev": true, "requires": { "aes-js": "3.0.0", - "bn.js": "4.11.8", + "bn.js": "^4.4.0", "elliptic": "6.5.2", "hash.js": "1.1.3", "js-sha3": "0.5.7", @@ -112,9 +118,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "ganache-cli": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/ganache-cli/-/ganache-cli-6.7.0.tgz", - "integrity": "sha512-9CZsClo9hl5MxGL7hkk14mie89Q94P0idh92jcV7LmppTYTCG7SHatuwcfqN7emFHArMt3fneN4QbH2do2N6Ow==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ganache-cli/-/ganache-cli-6.9.1.tgz", + "integrity": "sha512-VPBumkNUZzXDRQwVOby5YyQpd5t1clkr06xMgB28lZdEIn5ht1GMwUskOTFOAxdkQ4J12IWP0gdeacVRGowqbA==", "dev": true, "requires": { "ethereumjs-util": "6.1.0", @@ -124,23 +130,20 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "bundled": true, "dev": true }, "ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "bundled": true, "dev": true, "requires": { - "color-convert": "1.9.3" + "color-convert": "^1.9.0" } }, "bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "bundled": true, "dev": true, "requires": { "file-uri-to-path": "1.0.0" @@ -148,82 +151,72 @@ }, "bip66": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "bundled": true, "dev": true, "requires": { - "safe-buffer": "5.2.0" + "safe-buffer": "^5.0.1" } }, "bn.js": { "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "bundled": true, "dev": true }, "brorand": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "bundled": true, "dev": true }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "bundled": true, "dev": true, "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.4", - "safe-buffer": "5.2.0" + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "buffer-from": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "bundled": true, "dev": true }, "buffer-xor": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "bundled": true, "dev": true }, "camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "bundled": true, "dev": true }, "cipher-base": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "bundled": true, "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "cliui": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "bundled": true, "dev": true, "requires": { - "string-width": "3.1.0", - "strip-ansi": "5.2.0", - "wrap-ansi": "5.1.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" } }, "color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "bundled": true, "dev": true, "requires": { "color-name": "1.1.3" @@ -231,116 +224,105 @@ }, "color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "bundled": true, "dev": true }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "bundled": true, "dev": true, "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.4", - "md5.js": "1.3.5", - "ripemd160": "2.0.2", - "sha.js": "2.4.11" + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "bundled": true, "dev": true, "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.4", - "ripemd160": "2.0.2", - "safe-buffer": "5.2.0", - "sha.js": "2.4.11" + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "cross-spawn": { "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "bundled": true, "dev": true, "requires": { - "nice-try": "1.0.5", - "path-key": "2.0.1", - "semver": "5.7.0", - "shebang-command": "1.2.0", - "which": "1.3.1" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, "decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "bundled": true, "dev": true }, "drbg.js": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", - "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "bundled": true, "dev": true, "requires": { - "browserify-aes": "1.2.0", - "create-hash": "1.2.0", - "create-hmac": "1.1.7" + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" } }, "elliptic": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", - "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "bundled": true, "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.7", - "hmac-drbg": "1.0.1", - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" } }, "emoji-regex": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "bundled": true, "dev": true }, "end-of-stream": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "bundled": true, "dev": true, "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, "ethereumjs-util": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", - "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "bundled": true, "dev": true, "requires": { - "bn.js": "4.11.8", - "create-hash": "1.2.0", + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", "ethjs-util": "0.1.6", - "keccak": "1.4.0", - "rlp": "2.2.3", - "safe-buffer": "5.2.0", - "secp256k1": "3.7.1" + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" } }, "ethjs-util": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", - "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "bundled": true, "dev": true, "requires": { "is-hex-prefixed": "1.0.0", @@ -349,454 +331,398 @@ }, "evp_bytestokey": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "bundled": true, "dev": true, "requires": { - "md5.js": "1.3.5", - "safe-buffer": "5.2.0" + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" } }, "execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "bundled": true, "dev": true, "requires": { - "cross-spawn": "6.0.5", - "get-stream": "4.1.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" } }, "file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "bundled": true, "dev": true }, "find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "bundled": true, "dev": true, "requires": { - "locate-path": "3.0.0" + "locate-path": "^3.0.0" } }, "get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "bundled": true, "dev": true }, "get-stream": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "bundled": true, "dev": true, "requires": { - "pump": "3.0.0" + "pump": "^3.0.0" } }, "hash-base": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "bundled": true, "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "hash.js": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "bundled": true, "dev": true, "requires": { - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, "hmac-drbg": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "bundled": true, "dev": true, "requires": { - "hash.js": "1.1.7", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, "inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "bundled": true, "dev": true }, "invert-kv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "bundled": true, "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "bundled": true, "dev": true }, "is-hex-prefixed": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", - "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=", + "bundled": true, "dev": true }, "is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "bundled": true, "dev": true }, "isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "bundled": true, "dev": true }, "keccak": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", - "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "bundled": true, "dev": true, "requires": { - "bindings": "1.5.0", - "inherits": "2.0.4", - "nan": "2.14.0", - "safe-buffer": "5.2.0" + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" } }, "lcid": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "bundled": true, "dev": true, "requires": { - "invert-kv": "2.0.0" + "invert-kv": "^2.0.0" } }, "locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "bundled": true, "dev": true, "requires": { - "p-locate": "3.0.0", - "path-exists": "3.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, "map-age-cleaner": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "bundled": true, "dev": true, "requires": { - "p-defer": "1.0.0" + "p-defer": "^1.0.0" } }, "md5.js": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "bundled": true, "dev": true, "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.4", - "safe-buffer": "5.2.0" + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "mem": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "bundled": true, "dev": true, "requires": { - "map-age-cleaner": "0.1.3", - "mimic-fn": "2.1.0", - "p-is-promise": "2.1.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" } }, "mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "bundled": true, "dev": true }, "minimalistic-assert": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "bundled": true, "dev": true }, "minimalistic-crypto-utils": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "bundled": true, "dev": true }, "nan": { "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "bundled": true, "dev": true }, "nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "bundled": true, "dev": true }, "npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "bundled": true, "dev": true, "requires": { - "path-key": "2.0.1" + "path-key": "^2.0.0" } }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "os-locale": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "bundled": true, "dev": true, "requires": { - "execa": "1.0.0", - "lcid": "2.0.0", - "mem": "4.3.0" + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "p-defer": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "bundled": true, "dev": true }, "p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "bundled": true, "dev": true }, "p-is-promise": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "bundled": true, "dev": true }, "p-limit": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "bundled": true, "dev": true, "requires": { - "p-try": "2.2.0" + "p-try": "^2.0.0" } }, "p-locate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "bundled": true, "dev": true, "requires": { - "p-limit": "2.2.0" + "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "bundled": true, "dev": true }, "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "bundled": true, "dev": true }, "path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "bundled": true, "dev": true }, "pump": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "bundled": true, "dev": true, "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "bundled": true, "dev": true }, "require-main-filename": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "bundled": true, "dev": true }, "ripemd160": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "bundled": true, "dev": true, "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.4" + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "rlp": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.3.tgz", - "integrity": "sha512-l6YVrI7+d2vpW6D6rS05x2Xrmq8oW7v3pieZOJKBEdjuTF4Kz/iwk55Zyh1Zaz+KOB2kC8+2jZlp2u9L4tTzCQ==", + "bundled": true, "dev": true, "requires": { - "bn.js": "4.11.8", - "safe-buffer": "5.2.0" + "bn.js": "^4.11.1", + "safe-buffer": "^5.1.1" } }, "safe-buffer": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "bundled": true, "dev": true }, "secp256k1": { "version": "3.7.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", - "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", + "bundled": true, "dev": true, "requires": { - "bindings": "1.5.0", - "bip66": "1.1.5", - "bn.js": "4.11.8", - "create-hash": "1.2.0", - "drbg.js": "1.0.1", - "elliptic": "6.5.0", - "nan": "2.14.0", - "safe-buffer": "5.2.0" + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.4.1", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" } }, "semver": { "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "bundled": true, "dev": true }, "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true, "dev": true }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "bundled": true, "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.2.0" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "bundled": true, "dev": true, "requires": { - "shebang-regex": "1.0.0" + "shebang-regex": "^1.0.0" } }, "shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "bundled": true, "dev": true }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true, "dev": true }, "source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "bundled": true, "dev": true }, "source-map-support": { "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "bundled": true, "dev": true, "requires": { - "buffer-from": "1.1.1", - "source-map": "0.6.1" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, "string-width": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "bundled": true, "dev": true, "requires": { - "emoji-regex": "7.0.3", - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "5.2.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, "strip-ansi": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "bundled": true, "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^4.1.0" } }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "bundled": true, "dev": true }, "strip-hex-prefix": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", - "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", + "bundled": true, "dev": true, "requires": { "is-hex-prefixed": "1.0.0" @@ -804,84 +730,77 @@ }, "which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "bundled": true, "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "which-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "bundled": true, "dev": true }, "wrap-ansi": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "bundled": true, "dev": true, "requires": { - "ansi-styles": "3.2.1", - "string-width": "3.1.0", - "strip-ansi": "5.2.0" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" } }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "bundled": true, "dev": true }, "y18n": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "bundled": true, "dev": true }, "yargs": { "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "bundled": true, "dev": true, "requires": { - "cliui": "5.0.0", - "find-up": "3.0.0", - "get-caller-file": "2.0.5", - "os-locale": "3.1.0", - "require-directory": "2.1.1", - "require-main-filename": "2.0.0", - "set-blocking": "2.0.0", - "string-width": "3.1.0", - "which-module": "2.0.0", - "y18n": "4.0.0", - "yargs-parser": "13.1.1" + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" } }, "yargs-parser": { "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "bundled": true, "dev": true, "requires": { - "camelcase": "5.3.1", - "decamelize": "1.2.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "growl": { @@ -900,8 +819,8 @@ "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", "dev": true, "requires": { - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" } }, "he": { @@ -915,9 +834,9 @@ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dev": true, "requires": { - "hash.js": "1.1.3", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, "inflight": { @@ -925,8 +844,8 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -940,6 +859,12 @@ "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -957,7 +882,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -995,6 +920,19 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } } } }, @@ -1008,7 +946,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "original-require": { @@ -1026,22 +964,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { - "glob": "7.1.6" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } + "glob": "^7.1.3" } }, "scrypt-js": { @@ -1061,19 +984,29 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } }, "truffle": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.4.tgz", - "integrity": "sha512-HlefSmFCK/vd5jrGNpsJvsAhFdR3XPJH63ERQ1BHmVs8u1kt8OomExEov5HsWKqYCZIyERS/XNL2p/PYFJlmiQ==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.16.tgz", + "integrity": "sha512-McY2AKicKIdIclpclIXluWtCf1yj2cTJGS0ghStHkklw7XYk3ZL4MoftNyfqiYCjIjGs24qoEF9o/JlRp5qnmQ==", "requires": { - "app-module-path": "2.2.0", + "app-module-path": "^2.2.0", "mocha": "5.2.0", "original-require": "1.0.1" } }, + "truffle-assertions": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/truffle-assertions/-/truffle-assertions-0.9.2.tgz", + "integrity": "sha512-9g2RhaxU2F8DeWhqoGQvL/bV8QVoSnQ6PY+ZPvYRP5eF7+/8LExb4mjLx/FeliLTjc3Tv1SABG05Gu5qQ/ErmA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "lodash.isequal": "^4.5.0" + } + }, "uuid": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", diff --git a/on-chain/evm-contracts/package.json b/on-chain/evm-contracts/package.json index cb8841b08..1d1157d81 100644 --- a/on-chain/evm-contracts/package.json +++ b/on-chain/evm-contracts/package.json @@ -13,11 +13,12 @@ "license": "ISC", "devDependencies": { "ethers": "^4.0.40", - "ganache-cli": "^6.7.0" + "ganache-cli": "^6.9.1", + "truffle-assertions": "^0.9.2" }, "dependencies": { "commander": "^4.0.1", "rimraf": "^3.0.2", - "truffle": "^5.1.4" + "truffle": "^5.1.15" } } diff --git a/on-chain/evm-contracts/test/chainBridge/createDepositProposal.js b/on-chain/evm-contracts/test/chainBridge/createDepositProposal.js new file mode 100644 index 000000000..2524839d2 --- /dev/null +++ b/on-chain/evm-contracts/test/chainBridge/createDepositProposal.js @@ -0,0 +1,388 @@ +/** + * Copyright 2020 ChainSafe Systems + * SPDX-License-Identifier: LGPL-3.0-only + */ + +const truffleAssert = require('truffle-assertions'); +const ethers = require('ethers'); + +const ValidatorContract = artifacts.require("Validator"); +const BridgeContract = artifacts.require("Bridge"); +const ERC20MintableContract = artifacts.require("ERC20Mintable"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); + +contract('Bridge - [createDepositProposal with validatorThreshold = 1]', async (accounts) => { + // const minterAndValidator = accounts[0]; + const originChainValidatorAddress = accounts[1]; + const originChainDepositerAddress = accounts[2]; + const destinationChainRecipientAddress = accounts[3]; + const originChainID = 0; + const destinationChainID = 0; + const originChainInitialTokenAmount = 100; + const depositAmount = 10; + const expectedDepositID = 1; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC20MintableInstance; + let OriginERC20HandlerInstance; + let DestinationERC20MintableInstance; + let DestinationERC20HandlerInstance; + let data = ''; + let dataHash = ''; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([originChainValidatorAddress], 1); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 1); + OriginERC20MintableInstance = await ERC20MintableContract.new(); + OriginERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + DestinationERC20MintableInstance = await ERC20MintableContract.new(); + DestinationERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + + await OriginERC20MintableInstance.mint(originChainDepositerAddress, originChainInitialTokenAmount); + await OriginERC20MintableInstance.approve(OriginERC20HandlerInstance.address, depositAmount, { from: originChainDepositerAddress }); + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + data = '0x' + ethers.utils.hexZeroPad(DestinationERC20MintableInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(DestinationERC20HandlerInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(ethers.utils.hexlify(depositAmount), 32).substr(2); + dataHash = ethers.utils.keccak256(data); + }); + + it('[sanity] ERC20 deposit record is created with expected depositID and values', async () => { + const expectedDepositRecord = { + _originChainTokenAddress: OriginERC20MintableInstance.address, + _originChainHandlerAddress: OriginERC20HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC20HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _amount: depositAmount + }; + + const depositRecord = await BridgeInstance._erc20DepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord[expectedProperty]}`) + } + }); + + it('depositProposal can be created', async () => { + truffleAssert.passes(await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + }); + + it('Only validators should be able to create a deposit proposal', async () => { + await truffleAssert.reverts(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainDepositerAddress } + )); + }); + + it('depositProposal is created with expected values', async () => { + const expectedDepositProposal = { + _originChainID: originChainID, + _depositID: expectedDepositID, + _dataHash: dataHash, + _numYes: 1, + _numNo: 0, + _status: 3 // passed + }; + + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + const depositProposal = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositProposal)) { + // Testing all expected object properties + assert.property(depositProposal, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositProposalValue = depositProposal[expectedProperty].toNumber !== undefined ? + depositProposal[expectedProperty].toNumber() : depositProposal[expectedProperty]; + assert.strictEqual( + depositProposalValue, expectedDepositProposal[expectedProperty], + `property: ${expectedProperty}'s value: ${depositProposalValue} does not match expected value: ${expectedDepositProposal[expectedProperty]}`) + } + }); + + it("depositProposal shouldn't be created if it doesn't have Inactive or Denied status", async () => { + await truffleAssert.passes(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + + await truffleAssert.reverts(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + }); + + it('originChainValidatorAddress should be marked as vote for proposal', async () => { + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + const hasVoted = await BridgeInstance.hasVoted(originChainID, expectedDepositID, originChainValidatorAddress); + assert.isTrue(hasVoted); + }); + + it('DepositProposalCreated event is fired with expected value', async () => { + const proposalTx = await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + truffleAssert.eventEmitted(proposalTx, 'DepositProposalCreated', (event) => { + return event.originChainID.toNumber() === originChainID && + event.depositID.toNumber() === expectedDepositID && + event.dataHash === dataHash + }); + }); + + it('getDepositProposal should return correct values in expected order', async () => { + const expectedDepositProposal = { + _originChainID: originChainID, + _depositID: expectedDepositID, + _dataHash: dataHash, + _numYes: 1, + _numNo: 0, + _status: 'passed' + }; + + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + const depositProposal = await BridgeInstance.getDepositProposal(destinationChainID, expectedDepositID); + const depositProposalValues = Object.values(depositProposal); + depositProposalValues.forEach((depositRecordValue, index) => { + depositProposalValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositProposalValues, Object.values(expectedDepositProposal)); + }); +}); + +contract('Bridge - [createDepositProposal with validatorThreshold > 1]', async (accounts) => { + // const minterAndValidator = accounts[0]; + const originChainValidatorAddress = accounts[1]; + const originChainDepositerAddress = accounts[2]; + const destinationChainRecipientAddress = accounts[3]; + const originChainID = 0; + const destinationChainID = 0; + const originChainInitialTokenAmount = 100; + const depositAmount = 10; + const expectedDepositID = 1; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC20MintableInstance; + let OriginERC20HandlerInstance; + let DestinationERC20MintableInstance; + let DestinationERC20HandlerInstance; + let data = ''; + let dataHash = ''; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([originChainValidatorAddress], 1); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 2); + OriginERC20MintableInstance = await ERC20MintableContract.new(); + OriginERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + DestinationERC20MintableInstance = await ERC20MintableContract.new(); + DestinationERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + + await OriginERC20MintableInstance.mint(originChainDepositerAddress, originChainInitialTokenAmount); + await OriginERC20MintableInstance.approve(OriginERC20HandlerInstance.address, depositAmount, { from: originChainDepositerAddress }); + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + data = '0x' + ethers.utils.hexZeroPad(DestinationERC20MintableInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(DestinationERC20HandlerInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(ethers.utils.hexlify(depositAmount), 32).substr(2); + dataHash = ethers.utils.keccak256(data); + }); + + it('[sanity] ERC20 deposit record is created with expected depositID and values', async () => { + const expectedDepositRecord = { + _originChainTokenAddress: OriginERC20MintableInstance.address, + _originChainHandlerAddress: OriginERC20HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC20HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _amount: depositAmount + }; + + const depositRecord = await BridgeInstance._erc20DepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord[expectedProperty]}`) + } + }); + + it('depositProposal can be created', async () => { + truffleAssert.passes(await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + }); + + it('Only validators should be able to create a deposit proposal', async () => { + await truffleAssert.reverts(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainDepositerAddress } + )); + }); + + it('depositProposal is created with expected values', async () => { + const expectedDepositProposal = { + _originChainID: originChainID, + _depositID: expectedDepositID, + _dataHash: dataHash, + _numYes: 1, + _numNo: 0, + _status: 1 // passed + }; + + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + const depositProposal = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositProposal)) { + // Testing all expected object properties + assert.property(depositProposal, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositProposalValue = depositProposal[expectedProperty].toNumber !== undefined ? + depositProposal[expectedProperty].toNumber() : depositProposal[expectedProperty]; + assert.strictEqual( + depositProposalValue, expectedDepositProposal[expectedProperty], + `property: ${expectedProperty}'s value: ${depositProposalValue} does not match expected value: ${expectedDepositProposal[expectedProperty]}`) + } + }); + + it("depositProposal shouldn't be created if it doesn't have Inactive or Denied status", async () => { + await truffleAssert.passes(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + + await truffleAssert.reverts(BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + )); + }); + + it('originChainValidatorAddress should be marked as voted for proposal', async () => { + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + const hasVoted = await BridgeInstance.hasVoted(originChainID, expectedDepositID, originChainValidatorAddress); + assert.isTrue(hasVoted); + }); + + it('DepositProposalCreated event is fired with expected value', async () => { + const proposalTx = await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + truffleAssert.eventEmitted(proposalTx, 'DepositProposalCreated', (event) => { + return event.originChainID.toNumber() === originChainID && + event.depositID.toNumber() === expectedDepositID && + event.dataHash === dataHash + }); + }); + + it('getDepositProposal should return correct values in expected order', async () => { + const expectedDepositProposal = { + _originChainID: originChainID, + _depositID: expectedDepositID, + _dataHash: dataHash, + _numYes: 1, + _numNo: 0, + _status: 'active' + }; + + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress } + ); + + const depositProposal = await BridgeInstance.getDepositProposal(destinationChainID, expectedDepositID); + const depositProposalValues = Object.values(depositProposal); + depositProposalValues.forEach((depositRecordValue, index) => { + depositProposalValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositProposalValues, Object.values(expectedDepositProposal)); + }); +}); \ No newline at end of file diff --git a/on-chain/evm-contracts/test/chainBridge/depositERC20.js b/on-chain/evm-contracts/test/chainBridge/depositERC20.js new file mode 100644 index 000000000..4cdbcd180 --- /dev/null +++ b/on-chain/evm-contracts/test/chainBridge/depositERC20.js @@ -0,0 +1,181 @@ +/** + * Copyright 2020 ChainSafe Systems + * SPDX-License-Identifier: LGPL-3.0-only + */ + +const truffleAssert = require('truffle-assertions'); + +const ValidatorContract = artifacts.require("Validator"); +const BridgeContract = artifacts.require("Bridge"); +const ERC20MintableContract = artifacts.require("ERC20Mintable"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); + +contract('Bridge - [depositERC20]', async (accounts) => { + // const minter = accounts[0]; + const originChainDepositerAddress = accounts[1]; + const destinationChainRecipientAddress = accounts[2]; + const destinationChainID = 0; + const originChainInitialTokenAmount = 100; + const depositAmount = 10; + const expectedDepositID = 1; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC20MintableInstance; + let OriginERC20HandlerInstance; + let DestinationERC20MintableInstance; + let DestinationERC20HandlerInstance; + let expectedDepositRecord; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([], 0); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 0); + OriginERC20MintableInstance = await ERC20MintableContract.new(); + OriginERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + DestinationERC20MintableInstance = await ERC20MintableContract.new(); + DestinationERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + + await OriginERC20MintableInstance.mint(originChainDepositerAddress, originChainInitialTokenAmount); + await OriginERC20MintableInstance.approve(OriginERC20HandlerInstance.address, depositAmount, { from: originChainDepositerAddress }); + + expectedDepositRecord = { + _originChainTokenAddress: OriginERC20MintableInstance.address, + _originChainHandlerAddress: OriginERC20HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC20HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _amount: depositAmount + }; + }); + + it("[sanity] test originChainDepositerAddress' balance", async () => { + const originChainDepositerBalance = await OriginERC20MintableInstance.balanceOf(originChainDepositerAddress); + assert.strictEqual(originChainDepositerBalance.toNumber(), originChainInitialTokenAmount); + }); + + it("[sanity] test OriginERC20HandlerInstance.address' allowance", async () => { + const originChainHandlerAllowance = await OriginERC20MintableInstance.allowance(originChainDepositerAddress, OriginERC20HandlerInstance.address); + assert.strictEqual(originChainHandlerAllowance.toNumber(), depositAmount); + }); + + it('ERC20 deposit can be made', async () => { + truffleAssert.passes(await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + )); + }); + + it('_depositCounts should be increments from 0 to 1', async () => { + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + const depositCount = await BridgeInstance._depositCounts.call(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('getDepositCounts should return correct expectedDepositID', async () => { + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + const depositCount = await BridgeInstance.getDepositCount(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('ERC20 can be deposited with correct balances', async () => { + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + const originChainDepositerBalance = await OriginERC20MintableInstance.balanceOf(originChainDepositerAddress); + assert.strictEqual(originChainDepositerBalance.toNumber(), originChainInitialTokenAmount - depositAmount); + + const originChainHandlerBalance = await OriginERC20MintableInstance.balanceOf(OriginERC20HandlerInstance.address); + assert.strictEqual(originChainHandlerBalance.toNumber(), depositAmount); + }); + + it('ERC20 deposit record is created with expected depositID and values', async () => { + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance._erc20DepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord[expectedProperty]}`) + } + }); + + it('ERC20Deposited event is fired with expected value', async () => { + const depositTx = await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + truffleAssert.eventEmitted(depositTx, 'ERC20Deposited', (event) => { + return event.depositID.toNumber() === expectedDepositID + }); + }); + + it('getERC20DepositRecord should return correct depositID with values in expected order', async () => { + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance.getERC20DepositRecord(destinationChainID, expectedDepositID); + const depositRecordValues = Object.values(depositRecord); + depositRecordValues.forEach((depositRecordValue, index) => { + depositRecordValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositRecordValues, Object.values(expectedDepositRecord)); + }); +}); \ No newline at end of file diff --git a/on-chain/evm-contracts/test/chainBridge/depositERC721.js b/on-chain/evm-contracts/test/chainBridge/depositERC721.js new file mode 100644 index 000000000..5cea8d818 --- /dev/null +++ b/on-chain/evm-contracts/test/chainBridge/depositERC721.js @@ -0,0 +1,197 @@ +/** + * Copyright 2020 ChainSafe Systems + * SPDX-License-Identifier: LGPL-3.0-only + */ + +const truffleAssert = require('truffle-assertions'); + +const ValidatorContract = artifacts.require("Validator"); +const BridgeContract = artifacts.require("Bridge"); +const ERC721MintableContract = artifacts.require("ERC721Mintable"); +const ERC721HandlerContract = artifacts.require("ERC721Handler"); + +contract('Bridge - [depositERC721]', async (accounts) => { + // const minter = accounts[0]; + const originChainDepositerAddress = accounts[1]; + const destinationChainRecipientAddress = accounts[2]; + const destinationChainID = 0; + const originChainTokenID = 42; + const expectedDepositID = 1; + const genericBytes = '0x736f796c656e745f677265656e5f69735f70656f706c65'; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC721MintableInstance; + let OriginERC721HandlerInstance; + let DestinationERC721MintableInstance; + let DestinationERC721HandlerInstance; + let expectedDepositRecord; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([], 0); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 0); + OriginERC721MintableInstance = await ERC721MintableContract.new(); + OriginERC721HandlerInstance = await ERC721HandlerContract.new(BridgeInstance.address); + DestinationERC721MintableInstance = await ERC721MintableContract.new(); + DestinationERC721HandlerInstance = await ERC721HandlerContract.new(BridgeInstance.address); + + await OriginERC721MintableInstance.safeMint(originChainDepositerAddress, originChainTokenID, genericBytes); + await OriginERC721MintableInstance.approve(OriginERC721HandlerInstance.address, originChainTokenID, { from: originChainDepositerAddress }); + + expectedDepositRecord = { + _originChainTokenAddress: OriginERC721MintableInstance.address, + _originChainHandlerAddress: OriginERC721HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC721HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _tokenID: originChainTokenID, + _data: genericBytes + }; + }); + + it("[sanity] test originChainDepositerAddress' balance", async () => { + const originChainDepositerBalance = await OriginERC721MintableInstance.balanceOf(originChainDepositerAddress); + assert.strictEqual(originChainDepositerBalance.toNumber(), 1); + }); + + it(`[sanity] test originChainDepositerAddress owns token with ID: ${originChainTokenID}`, async () => { + const tokenOwner = await OriginERC721MintableInstance.ownerOf(originChainTokenID); + assert.strictEqual(tokenOwner, originChainDepositerAddress); + }); + + it("[sanity] test OriginERC721HandlerInstance.address' allowance", async () => { + const allowanceHolder = await OriginERC721MintableInstance.getApproved(originChainTokenID); + assert.strictEqual(allowanceHolder, OriginERC721HandlerInstance.address); + }); + + it('ERC721 deposit can be made', async () => { + truffleAssert.passes(await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + )); + }); + + it('_depositCounts should be increments from 0 to 1', async () => { + await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositCount = await BridgeInstance._depositCounts.call(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('getDepositCounts should return correct expectedDepositID', async () => { + await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositCount = await BridgeInstance.getDepositCount(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('ERC721 can be deposited with correct owner and balances', async () => { + await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + const tokenOwner = await OriginERC721MintableInstance.ownerOf(originChainTokenID); + assert.strictEqual(tokenOwner, OriginERC721HandlerInstance.address); + + const originChainDepositerBalance = await OriginERC721MintableInstance.balanceOf(originChainDepositerAddress); + assert.strictEqual(originChainDepositerBalance.toNumber(), 0); + + const originChainHandlerBalance = await OriginERC721MintableInstance.balanceOf(OriginERC721HandlerInstance.address); + assert.strictEqual(originChainHandlerBalance.toNumber(), 1); + }); + + it('ERC721 deposit record is created with expected depositID and values', async () => { + await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance._erc721DepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord[expectedProperty]}`) + } + }); + + it('ERC721Deposited event is fired with expected value', async () => { + const depositTx = await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + truffleAssert.eventEmitted(depositTx, 'ERC721Deposited', (event) => { + return event.depositID.toNumber() === expectedDepositID + }); + }); + + it('getERC721DepositRecord should return correct depositID with values in expected order', async () => { + await BridgeInstance.depositERC721( + OriginERC721MintableInstance.address, + OriginERC721HandlerInstance.address, + destinationChainID, + DestinationERC721HandlerInstance.address, + destinationChainRecipientAddress, + originChainTokenID, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance.getERC721DepositRecord(destinationChainID, expectedDepositID); + const depositRecordValues = Object.values(depositRecord); + depositRecordValues.forEach((depositRecordValue, index) => { + depositRecordValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositRecordValues, Object.values(expectedDepositRecord)); + }); +}); \ No newline at end of file diff --git a/on-chain/evm-contracts/test/chainBridge/depositGeneric.js b/on-chain/evm-contracts/test/chainBridge/depositGeneric.js new file mode 100644 index 000000000..6d554578c --- /dev/null +++ b/on-chain/evm-contracts/test/chainBridge/depositGeneric.js @@ -0,0 +1,212 @@ +/** + * Copyright 2020 ChainSafe Systems + * SPDX-License-Identifier: LGPL-3.0-only + */ + +const truffleAssert = require('truffle-assertions'); + +const ValidatorContract = artifacts.require("Validator"); +const BridgeContract = artifacts.require("Bridge"); +const ERC20MintableContract = artifacts.require("ERC20Mintable"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); + +contract('Bridge - [depositGeneric]', async (accounts) => { + // const minter = accounts[0]; + const originChainDepositerAddress = accounts[1]; + const destinationChainRecipientAddress = accounts[2]; + const destinationChainID = 0; + const expectedDepositID = 1; + const genericBytes = '0x736f796c656e745f677265656e5f69735f70656f706c65'; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC20MintableInstance; + let OriginERC20HandlerInstance; + let DestinationERC20MintableInstance; + let DestinationERC20HandlerInstance; + let expectedDepositRecord_PartialArguments; + let expectedDepositRecord_AllArguments; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([], 0); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 0); + OriginERC20MintableInstance = await ERC20MintableContract.new(); + OriginERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + DestinationERC20MintableInstance = await ERC20MintableContract.new(); + DestinationERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + + expectedDepositRecord_PartialArguments = { + _originChainTokenAddress: '0x0000000000000000000000000000000000000000', + _originChainHandlerAddress: '0x0000000000000000000000000000000000000000', + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: '0x0000000000000000000000000000000000000000', + _destinationRecipientAddress: destinationChainRecipientAddress, + _data: genericBytes + }; + + expectedDepositRecord_AllArguments = { + _originChainTokenAddress: OriginERC20MintableInstance.address, + _originChainHandlerAddress: OriginERC20HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC20HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _data: genericBytes + }; + }); + + it('Generic deposit can be made with partial arguments', async () => { + truffleAssert.passes(await BridgeInstance.depositGeneric( + destinationChainID, + destinationChainRecipientAddress, + genericBytes + )); + }); + + it('_depositCounts is incremented correctly after Generic deposit with partial arguments', async () => { + await BridgeInstance.depositGeneric( + destinationChainID, + destinationChainRecipientAddress, + genericBytes + ); + + const depositCount = await BridgeInstance._depositCounts.call(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('Generic deposit with partial arguments is stored correctly', async () => { + await BridgeInstance.depositGeneric( + destinationChainID, + destinationChainRecipientAddress, + genericBytes + ); + + const depositRecord = await BridgeInstance._genericDepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord_PartialArguments)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord_PartialArguments[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord_PartialArguments[expectedProperty]}`) + } + }); + + it('GenericDeposited event is fired with expected value after Generic deposit with partial arguments', async () => { + const depositTx = await BridgeInstance.depositGeneric( + destinationChainID, + destinationChainRecipientAddress, + genericBytes + ); + + truffleAssert.eventEmitted(depositTx, 'GenericDeposited', (event) => { + return event.depositID.toNumber() === expectedDepositID + }); + }); + + it('getGenericDepositRecord should return correct depositID with values in expected order for Generic deposit with partial arguments', async () => { + await BridgeInstance.depositGeneric( + destinationChainID, + destinationChainRecipientAddress, + genericBytes + ); + + const depositRecord = await BridgeInstance.getGenericDepositRecord(destinationChainID, expectedDepositID); + const depositRecordValues = Object.values(depositRecord); + depositRecordValues.forEach((depositRecordValue, index) => { + depositRecordValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositRecordValues, Object.values(expectedDepositRecord_PartialArguments)); + }); + + it('Generic deposit can be made with all arguments', async () => { + truffleAssert.passes(await BridgeInstance.depositGeneric( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + genericBytes, + { from: originChainDepositerAddress } + )); + }); + + it('_depositCounts is incremented correctly after Generic deposit with all arguments', async () => { + await BridgeInstance.depositGeneric( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositCount = await BridgeInstance._depositCounts.call(destinationChainID); + assert.strictEqual(depositCount.toNumber(), expectedDepositID); + }); + + it('Generic deposit with all arguments is stored correctly', async () => { + await BridgeInstance.depositGeneric( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance._genericDepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord_AllArguments)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord_AllArguments[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord_AllArguments[expectedProperty]}`) + } + }); + + it('GenericDeposited event is fired with expected value after Generic deposit with all arguments', async () => { + const depositTx = await BridgeInstance.depositGeneric( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + genericBytes, + { from: originChainDepositerAddress } + ); + + truffleAssert.eventEmitted(depositTx, 'GenericDeposited', (event) => { + return event.depositID.toNumber() === expectedDepositID + }); + }); + + it('getGenericDepositRecord should return correct depositID with values in expected order for Generic deposit with all arguments', async () => { + await BridgeInstance.depositGeneric( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + genericBytes, + { from: originChainDepositerAddress } + ); + + const depositRecord = await BridgeInstance.getGenericDepositRecord(destinationChainID, expectedDepositID); + const depositRecordValues = Object.values(depositRecord); + depositRecordValues.forEach((depositRecordValue, index) => { + depositRecordValues[index] = depositRecordValue.toNumber !== undefined ? + depositRecordValue.toNumber() : depositRecordValue; + }); + assert.sameOrderedMembers(depositRecordValues, Object.values(expectedDepositRecord_AllArguments)); + }); +}); \ No newline at end of file diff --git a/on-chain/evm-contracts/test/chainBridge/voteDepositProposal.js b/on-chain/evm-contracts/test/chainBridge/voteDepositProposal.js new file mode 100644 index 000000000..4b235caf7 --- /dev/null +++ b/on-chain/evm-contracts/test/chainBridge/voteDepositProposal.js @@ -0,0 +1,298 @@ +/** + * Copyright 2020 ChainSafe Systems + * SPDX-License-Identifier: LGPL-3.0-only + */ + +const truffleAssert = require('truffle-assertions'); +const ethers = require('ethers'); + +const ValidatorContract = artifacts.require("Validator"); +const BridgeContract = artifacts.require("Bridge"); +const ERC20MintableContract = artifacts.require("ERC20Mintable"); +const ERC20HandlerContract = artifacts.require("ERC20Handler"); + +contract('Bridge - [voteDepositProposal with validatorThreshold > 1]', async (accounts) => { + // const minterAndValidator = accounts[0]; + const originChainValidatorAddress = accounts[1]; + const originChainValidator2Address = accounts[4]; + const originChainValidator3Address = accounts[5]; + const originChainDepositerAddress = accounts[2]; + const destinationChainRecipientAddress = accounts[3]; + const originChainID = 0; + const destinationChainID = 0; + const originChainInitialTokenAmount = 100; + const depositAmount = 10; + const expectedDepositID = 1; + + let ValidatorInstance; + let BridgeInstance; + let OriginERC20MintableInstance; + let OriginERC20HandlerInstance; + let DestinationERC20MintableInstance; + let DestinationERC20HandlerInstance; + let data = ''; + let dataHash = ''; + + beforeEach(async () => { + ValidatorInstance = await ValidatorContract.new([ + originChainValidatorAddress, + originChainValidator2Address, + originChainValidator3Address], 1); + BridgeInstance = await BridgeContract.new(ValidatorInstance.address, 2); + OriginERC20MintableInstance = await ERC20MintableContract.new(); + OriginERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + DestinationERC20MintableInstance = await ERC20MintableContract.new(); + DestinationERC20HandlerInstance = await ERC20HandlerContract.new(BridgeInstance.address); + + await OriginERC20MintableInstance.mint(originChainDepositerAddress, originChainInitialTokenAmount); + await OriginERC20MintableInstance.approve(OriginERC20HandlerInstance.address, depositAmount, { from: originChainDepositerAddress }); + await BridgeInstance.depositERC20( + OriginERC20MintableInstance.address, + OriginERC20HandlerInstance.address, + destinationChainID, + DestinationERC20HandlerInstance.address, + destinationChainRecipientAddress, + depositAmount, + { from: originChainDepositerAddress } + ); + + data = '0x' + ethers.utils.hexZeroPad(DestinationERC20MintableInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(DestinationERC20HandlerInstance.address, 32).substr(2) + + ethers.utils.hexZeroPad(ethers.utils.hexlify(depositAmount), 32).substr(2); + dataHash = ethers.utils.keccak256(data); + + await BridgeInstance.createDepositProposal( + originChainID, + expectedDepositID, + dataHash, + { from: originChainValidatorAddress }); + }); + + it('[sanity] ERC20 deposit record is created with expected depositID and values', async () => { + const expectedDepositRecord = { + _originChainTokenAddress: OriginERC20MintableInstance.address, + _originChainHandlerAddress: OriginERC20HandlerInstance.address, + _destinationChainID: destinationChainID, + _destinationChainHandlerAddress: DestinationERC20HandlerInstance.address, + _destinationRecipientAddress: destinationChainRecipientAddress, + _amount: depositAmount + }; + + const depositRecord = await BridgeInstance._erc20DepositRecords.call(destinationChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositRecord)) { + // Testing all expected object properties + assert.property(depositRecord, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositRecordValue = depositRecord[expectedProperty].toNumber !== undefined ? + depositRecord[expectedProperty].toNumber() : depositRecord[expectedProperty]; + assert.strictEqual( + depositRecordValue, expectedDepositRecord[expectedProperty], + `property: ${expectedProperty}'s value: ${depositRecordValue} does not match expected value: ${expectedDepositRecord[expectedProperty]}`) + } + }); + + it('[sanity] depositProposal is created with expected values', async () => { + const expectedDepositProposal = { + _originChainID: originChainID, + _depositID: expectedDepositID, + _dataHash: dataHash, + _numYes: 1, + _numNo: 0, + _status: 1 // passed + }; + + const depositProposal = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + for (const expectedProperty of Object.keys(expectedDepositProposal)) { + // Testing all expected object properties + assert.property(depositProposal, expectedProperty, `property: ${expectedProperty} does not exist in depositRecord`); + + // Testing all expected object values + const depositProposalValue = depositProposal[expectedProperty].toNumber !== undefined ? + depositProposal[expectedProperty].toNumber() : depositProposal[expectedProperty]; + assert.strictEqual( + depositProposalValue, expectedDepositProposal[expectedProperty], + `property: ${expectedProperty}'s value: ${depositProposalValue} does not match expected value: ${expectedDepositProposal[expectedProperty]}`) + } + }); + + it('depositProposal can be voted on', async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + }); + + it('Only validators should be able to vote on a deposit proposal', async () => { + await truffleAssert.reverts(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainDepositerAddress } + )); + }); + + it('Can only vote on a proposal that has active status', async () => { + await truffleAssert.reverts(BridgeInstance.voteDepositProposal( + originChainID, + 400, // fake depositID + 1, // vote in favor + { from: originChainValidator2Address } + )); + }); + + it("Can only vote on a proposal if validator hasn't already voted for it", async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + + await truffleAssert.reverts(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + }); + + it('Validator must provide a valid vote', async () => { + await truffleAssert.fails(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 42, // invalid vote, out of range + { from: originChainValidator2Address } + ), truffleAssert.ErrorType.INVALID_OPCODE); + }); + + it("Validator's vote should be recorded correctly - yes vote", async () => { + const depositProposalBeforeSecondVote = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + assert.strictEqual(depositProposalBeforeSecondVote._numYes.toNumber(), 1); + assert.strictEqual(depositProposalBeforeSecondVote._numNo.toNumber(), 0); + + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + + const depositProposalAfterSecondVote = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + assert.strictEqual(depositProposalAfterSecondVote._numYes.toNumber(), 2); + assert.strictEqual(depositProposalAfterSecondVote._numNo.toNumber(), 0); + }); + + it("Validator's vote should be recorded correctly - no vote", async () => { + const depositProposalBeforeSecondVote = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + assert.strictEqual(depositProposalBeforeSecondVote._numYes.toNumber(), 1); + assert.strictEqual(depositProposalBeforeSecondVote._numNo.toNumber(), 0); + + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote against + { from: originChainValidator2Address } + )); + + const depositProposalAfterSecondVote = await BridgeInstance._depositProposals.call(originChainID, expectedDepositID); + assert.strictEqual(depositProposalAfterSecondVote._numYes.toNumber(), 1); + assert.strictEqual(depositProposalAfterSecondVote._numNo.toNumber(), 1); + }); + + it("Validator's address should be marked as voted for proposal", async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + + const hasVoted = await BridgeInstance.hasVoted(originChainID, expectedDepositID, originChainValidator2Address); + assert.isTrue(hasVoted); + }); + + it('Proposal status should be updated to passed after numYes >= validatorThreshold', async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + )); + + const depositProposal = await BridgeInstance._depositProposals(originChainID, expectedDepositID); + assert.strictEqual(depositProposal._status.toNumber(), 3); + }); + + it('DepositProposalFinalized event fired when proposal status updated to passed after numYes >= validatorThreshold', async () => { + const voteTx = await BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 1, // vote in favor + { from: originChainValidator2Address } + ); + + truffleAssert.eventEmitted(voteTx, 'DepositProposalFinalized', (event) => { + return event.originChainID.toNumber() === originChainID && + event.depositID.toNumber() === expectedDepositID + }); + }); + + it('Proposal status should be updated to denied if majority of validators vote no', async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote in favor + { from: originChainValidator2Address } + )); + + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote in favor + { from: originChainValidator3Address } + )); + + const depositProposal = await BridgeInstance._depositProposals(originChainID, expectedDepositID); + assert.strictEqual(depositProposal._status.toNumber(), 2); + }); + + it('DepositProposalFinalized event fired when proposal status updated to denied if majority of validators vote no', async () => { + await truffleAssert.passes(BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote in favor + { from: originChainValidator2Address } + )); + + const voteTx = await BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote in favor + { from: originChainValidator3Address } + ); + + truffleAssert.eventEmitted(voteTx, 'DepositProposalFinalized', (event) => { + return event.originChainID.toNumber() === originChainID && + event.depositID.toNumber() === expectedDepositID + }); + }); + + it('DepositProposalVote event fired when proposal vote made', async () => { + const voteTx = await BridgeInstance.voteDepositProposal( + originChainID, + expectedDepositID, + 0, // vote in favor + { from: originChainValidator2Address } + ); + + truffleAssert.eventEmitted(voteTx, 'DepositProposalVote', (event) => { + return event.originChainID.toNumber() === originChainID && + event.depositID.toNumber() === expectedDepositID && + event.vote.toNumber() === 0 && + event.status.toNumber() === 1 + }); + }); +}); \ No newline at end of file