diff --git a/contracts/Forwarder.sol b/contracts/Forwarder.sol index b4360f0..130922b 100644 --- a/contracts/Forwarder.sol +++ b/contracts/Forwarder.sol @@ -15,11 +15,20 @@ contract Forwarder is AccessControlEnumerable, Pausable { bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); address public resultManager; - mapping(bytes32 => bytes) public collectionPayload; + bytes4 public resultGetterSelector; + bytes4 public updateSelector; + bytes4 public validateSelector; event PermissionSet(address sender); event PermissionRemoved(address sender); + error NoSelectorPresent(); + + modifier checkSelector(bytes4 selector) { + if(selector == bytes4(0)) revert NoSelectorPresent(); + _; + } + constructor(address _resultManager) { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); resultManager = _resultManager; @@ -32,44 +41,107 @@ contract Forwarder is AccessControlEnumerable, Pausable { external onlyRole(FORWARDER_ADMIN_ROLE) { - require(_resultManager.isContract(), "Not a contract address"); resultManager = _resultManager; } - /// @notice Set collection payload - /// @dev Allows admin to set collection payload - /// @param _collectionName keccak256 hash of collection name - /// @param _payload payload to call - function setCollectionPayload( - bytes32 _collectionName, - bytes memory _payload + /// @notice Set resultGetter Selector + /// @dev Allows admin to set resultGetter Selector + /// @param _resultGetterSelector resultGetter Selector + function setResultGetterSelector( + bytes4 _resultGetterSelector ) external onlyRole(FORWARDER_ADMIN_ROLE) { - collectionPayload[_collectionName] = _payload; + resultGetterSelector = _resultGetterSelector; + } + + /// @notice Set update selector + /// @dev Allows admin to set update selector + /// @param _updateSelector update selector + function setUpdateSelector(bytes4 _updateSelector) + external + onlyRole(FORWARDER_ADMIN_ROLE) + { + updateSelector = _updateSelector; } + /// @notice Set validate selector + /// @dev Allows admin to set validate selector + /// @param _validateSelector validate selector + function setValidateSelector(bytes4 _validateSelector) + external + onlyRole(FORWARDER_ADMIN_ROLE) + { + validateSelector = _validateSelector; + } + + /// @notice pause the contract function pause() external onlyRole(PAUSE_ROLE) { Pausable._pause(); } + /// @notice unpause the contract function unpause() external onlyRole(PAUSE_ROLE) { Pausable._unpause(); } - /// @notice get result by collection name - function getResult(bytes32 collectionName) + /** + * @notice Updates the result based on the provided data and returns the latest result + * @param data bytes data required to update the result + * @return result of the collection, its power and timestamp + */ + function fetchResult(bytes calldata data) external - view whenNotPaused + checkSelector(updateSelector) onlyRole(TRANSPARENT_FORWARDER_ROLE) - returns (uint256, int8) + returns (uint256, int8, uint256) { - require( - collectionPayload[collectionName].length > 0, - "Invalid collection name" + bytes memory returnData = resultManager.functionCall( + abi.encodePacked( + updateSelector, + data + ) ); - bytes memory data = resultManager.functionStaticCall( - collectionPayload[collectionName] + return abi.decode(returnData, (uint256, int8, uint256)); + } + + /** + * @dev using the hash of collection name, clients can query the result of that collection + * @param name bytes32 hash of the collection name + * @return result of the collection and its power + */ + function getResult(bytes32 name) + external + view + whenNotPaused + checkSelector(resultGetterSelector) + onlyRole(TRANSPARENT_FORWARDER_ROLE) + returns (uint256, int8, uint256) + { + bytes memory returnData = resultManager.functionStaticCall( + abi.encodePacked( + resultGetterSelector, + name + ) ); - return abi.decode(data, (uint256, int8)); + return abi.decode(returnData, (uint256, int8, uint256)); + } + + /** + * @dev validates the result based on the provided data and returns the validity + * @param data bytes data required to validate the result + * @return validity of the result + */ + function validateResult(bytes calldata data) + external + view + whenNotPaused + checkSelector(validateSelector) + onlyRole(TRANSPARENT_FORWARDER_ROLE) + returns (bool) + { + bytes memory returnData = resultManager.functionStaticCall( + abi.encodePacked(validateSelector, data) + ); + return abi.decode(returnData, (bool)); } } diff --git a/contracts/ResultManager.sol b/contracts/ResultManager.sol index 577dbb7..67e1d13 100644 --- a/contracts/ResultManager.sol +++ b/contracts/ResultManager.sol @@ -3,18 +3,15 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; contract ResultManager is AccessControlEnumerable { - struct Block { - bytes message; // epoch, timestamp, Value[] - bytes signature; - } - struct Value { int8 power; uint16 collectionId; bytes32 name; uint256 value; + uint256 lastUpdatedTimestamp; } bytes32 public constant RESULT_MANAGER_ADMIN_ROLE = @@ -22,16 +19,9 @@ contract ResultManager is AccessControlEnumerable { bytes32 public constant FORWARDER_ROLE = keccak256("FORWARDER_ROLE"); address public signerAddress; - uint256 public lastUpdatedTimestamp; - uint32 public latestEpoch; - - /// @notice mapping for name of collection in bytes32 -> collectionid - mapping(bytes32 => uint16) public collectionIds; /// @notice mapping for CollectionID -> Value Info - mapping(uint16 => Value) private _collectionResults; - - event BlockReceived(Block messageBlock); + mapping(bytes32 => Value) private _collectionResults; event SignerUpdated( address sender, @@ -39,11 +29,19 @@ contract ResultManager is AccessControlEnumerable { address indexed newSigner ); + event ResultUpdated(Value value); + + error InvalidSignature(); + error InvalidMerkleProof(); + constructor(address _signerAddress) { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); signerAddress = _signerAddress; } + /** + * @notice Updates the signer address + */ function updateSignerAddress( address _signerAddress ) external onlyRole(RESULT_MANAGER_ADMIN_ROLE) { @@ -51,36 +49,77 @@ contract ResultManager is AccessControlEnumerable { signerAddress = _signerAddress; } + /** @notice Updates the result based on the provided Merkle proof and decoded result. Regardless of whether the result + * is updated, a result will be returned. + * @param merkleRoot The root of the Merkle tree + * @param proof The Merkle proof for the result + * @param result The decoded result + * @param signature The signature for the result + * @return result of the collection, its power and timestamp + */ + function updateResult( + bytes32 merkleRoot, + bytes32[] memory proof, + Value memory result, + bytes memory signature + ) external onlyRole(FORWARDER_ROLE) returns (uint256, int8, uint256) { + if (result.lastUpdatedTimestamp > _collectionResults[result.name].lastUpdatedTimestamp) { + bytes memory resultBytes = abi.encode(result); + bytes32 messageHash = keccak256( + abi.encodePacked(merkleRoot, resultBytes) + ); + if( + ECDSA.recover( + ECDSA.toEthSignedMessageHash(messageHash), + signature + ) != signerAddress) revert InvalidSignature(); + + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(resultBytes))) + ); + if( + !MerkleProof.verify(proof, merkleRoot, leaf) + ) revert InvalidMerkleProof(); + _collectionResults[result.name] = result; + + emit ResultUpdated(result); + } + + return _getResult(result.name); + } + /** - * @dev Verify the signature and update the results - * Requirements: - * - * - ecrecover(signature) should match with signerAddress + * @dev validates the result based on the provided data and returns the validity + * @param merkleRoot The root of the Merkle tree + * @param proof The Merkle proof for the result + * @param result The decoded result + * @param signature The signature for the result + * @return validity of the result */ - function setBlock(Block memory messageBlock) external { - (uint32 epoch, uint256 timestamp, Value[] memory values) = abi.decode( - messageBlock.message, - (uint32, uint256, Value[]) + function validateResult( + bytes32 merkleRoot, + bytes32[] memory proof, + Value memory result, + bytes memory signature + ) external view onlyRole(FORWARDER_ROLE) returns (bool) { + bytes memory resultBytes = abi.encode(result); + bytes32 messageHash = keccak256( + abi.encodePacked(merkleRoot, resultBytes) ); - require(epoch > latestEpoch, "epoch must be > latestEpoch"); - bytes32 messageHash = keccak256(messageBlock.message); - require( + if ( ECDSA.recover( ECDSA.toEthSignedMessageHash(messageHash), - messageBlock.signature - ) == signerAddress, - "invalid signature" - ); + signature + ) != signerAddress + ) return false; - for (uint256 i; i < values.length; i++) { - _collectionResults[values[i].collectionId] = values[i]; - collectionIds[values[i].name] = values[i].collectionId; - } - lastUpdatedTimestamp = timestamp; - latestEpoch = epoch; + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(resultBytes))) + ); + if (!MerkleProof.verify(proof, merkleRoot, leaf)) return false; - emit BlockReceived(messageBlock); + return true; } /** @@ -90,17 +129,20 @@ contract ResultManager is AccessControlEnumerable { */ function getResult( bytes32 _name - ) external view onlyRole(FORWARDER_ROLE) returns (uint256, int8) { - uint16 id = collectionIds[_name]; - return _getResultFromID(id); + ) external view onlyRole(FORWARDER_ROLE) returns (uint256, int8, uint256) { + return _getResult(_name); } /** - * @dev Returns collection result and power with collectionId as parameter + * @dev internal function where using the hash of collection name, clients can query the + * result of that collection + * @param _name bytes32 hash of the collection name + * @return result of the collection and its power */ - function _getResultFromID( - uint16 _id - ) internal view returns (uint256, int8) { - return (_collectionResults[_id].value, _collectionResults[_id].power); + function _getResult( + bytes32 _name + ) internal view returns (uint256, int8, uint256) { + Value memory result = _collectionResults[_name]; + return (result.value, result.power, result.lastUpdatedTimestamp); } } diff --git a/contracts/interface/IDelegator.sol b/contracts/interface/IDelegator.sol index 636c582..bd888c6 100644 --- a/contracts/interface/IDelegator.sol +++ b/contracts/interface/IDelegator.sol @@ -3,80 +3,31 @@ pragma solidity ^0.8.0; interface IDelegator { /** - * @dev updates the address of the Collection Manager contract from where the delegator will fetch - * results of the oracle - * @param newDelegateAddress address of the Collection Manager - * @param newRandomNoManagerAddress address of the Random Number Manager + * @notice Updates the result based on the provided data and returns the latest result + * @dev The data will be updated only if the result is valid and is newer than the previous result. + * Updation will be done by the clients, though once the result is updated, it wont be updated till the latest results + * are sent again. Regardless of the updation, the result will be returned. + * @param _data bytes data required to update the result + * @return result of the collection, its power and timestamp */ - function updateAddress( - address newDelegateAddress, - address newRandomNoManagerAddress - ) external; - - /** - * @notice Allows Client to register for random number - * Per request a rquest id is generated, which is binded to one epoch - * this epoch is current epoch if Protocol is in commit state, or epoch + 1 if in any other state - * @return requestId : unique request id - */ - function register() external returns (bytes32); - - /** - * @dev using the hash of collection name, clients can query collection id with respect to its hash - * @param _name bytes32 hash of the collection name - * @return collection ID - */ - function getCollectionID(bytes32 _name) external view returns (uint16); + function fetchResult( + bytes calldata _data + ) external payable returns (uint256, int8, uint256); /** * @dev using the hash of collection name, clients can query the result of that collection * @param _name bytes32 hash of the collection name * @return result of the collection and its power */ - function getResult(bytes32 _name) external view returns (uint256, int8); - - /** - * @dev using the collection id, clients can query the result of the collection - * @param _id collection ID - * @return result of the collection and its power - */ - function getResultFromID(uint16 _id) external view returns (uint256, int8); - - /** - * @return ids of active collections in the oracle - */ - function getActiveCollections() external view returns (uint16[] memory); - - /** - * @dev using the collection id, clients can query the status of collection - * @param _id collection ID - * @return status of the collection - */ - function getCollectionStatus(uint16 _id) external view returns (bool); - - /** - * @notice Allows client to pull random number once available - * Random no is generated from secret of that epoch and request id, its unique per requestid - * @param requestId : A unique id per request - */ - function getRandomNumber(bytes32 requestId) external view returns (uint256); - - /** - * @notice Fetch generic random number of last epoch - * @return random number - */ - function getGenericRandomNumberOfLastEpoch() - external - view - returns (uint256); + function getResult( + bytes32 _name + ) external view returns (uint256, int8, uint256); /** - * @dev using epoch, clients can query random number generated of the epoch - * @param _epoch epoch - * @return random number + * @dev validates the result based on the provided data and returns the validity + * @param _data bytes data required to validate the result + * @return validity of the result */ - function getGenericRandomNumber(uint32 _epoch) - external - view - returns (uint256); + function validateResult(bytes calldata _data) + external view returns (bool); } diff --git a/contracts/mocks/Client.sol b/contracts/mocks/Client.sol index 8bd79f1..8cde233 100644 --- a/contracts/mocks/Client.sol +++ b/contracts/mocks/Client.sol @@ -3,88 +3,40 @@ pragma solidity ^0.8.0; interface IDelegator { /** - * @dev updates the address of the Collection Manager contract from where the delegator will fetch - * results of the oracle - * @param newDelegateAddress address of the Collection Manager - * @param newRandomNoManagerAddress address of the Random Number Manager + * @notice Updates the result based on the provided data and returns the latest result + * @dev The data will be updated only if the result is valid and is newer than the previous result. + * Updation will be done by the clients, though once the result is updated, it wont be updated till the latest results + * are sent again. Regardless of the updation, the result will be returned. + * @param _data bytes32 hash of the collection name + * @return result of the collection, its power and timestamp */ - function updateAddress( - address newDelegateAddress, - address newRandomNoManagerAddress - ) external; + function fetchResult( + bytes calldata _data + ) external payable returns (uint256, int8, uint256); /** * @dev using the hash of collection name, clients can query the result of that collection * @param _name bytes32 hash of the collection name * @return result of the collection and its power */ - function getResult(bytes32 _name) external payable returns (uint256, int8); + function getResult( + bytes32 _name + ) external view returns (uint256, int8, uint256); /** - * @notice Allows Client to register for random number - * Per request a rquest id is generated, which is binded to one epoch - * this epoch is current epoch if Protocol is in commit state, or epoch + 1 if in any other state - * @return requestId : unique request id + * @dev validates the result based on the provided data and returns the validity + * @param _data bytes data required to validate the result + * @return validity of the result */ - function register() external returns (bytes32); - - /** - * @dev using the hash of collection name, clients can query collection id with respect to its hash - * @param _name bytes32 hash of the collection name - * @return collection ID - */ - function getCollectionID(bytes32 _name) external view returns (uint16); - - /** - * @dev using the collection id, clients can query the result of the collection - * @param _id collection ID - * @return result of the collection and its power - */ - function getResultFromID(uint16 _id) external view returns (uint256, int8); - - /** - * @return ids of active collections in the oracle - */ - function getActiveCollections() external view returns (uint16[] memory); - - /** - * @dev using the collection id, clients can query the status of collection - * @param _id collection ID - * @return status of the collection - */ - function getCollectionStatus(uint16 _id) external view returns (bool); - - /** - * @notice Allows client to pull random number once available - * Random no is generated from secret of that epoch and request id, its unique per requestid - * @param requestId : A unique id per request - */ - function getRandomNumber(bytes32 requestId) external view returns (uint256); - - /** - * @notice Fetch generic random number of last epoch - * @return random number - */ - function getGenericRandomNumberOfLastEpoch() - external - view - returns (uint256); - - /** - * @dev using epoch, clients can query random number generated of the epoch - * @param _epoch epoch - * @return random number - */ - function getGenericRandomNumber(uint32 _epoch) - external - view - returns (uint256); + function validateResult(bytes calldata _data) + external view returns (bool); } contract Client { IDelegator public transparentForwarder; uint256 public lastResult; int8 public lastPower; + uint256 public lastTimestamp; constructor(address _transparentForwarder) { transparentForwarder = IDelegator(_transparentForwarder); @@ -94,13 +46,32 @@ contract Client { transparentForwarder = IDelegator(_transparentForwarder); } - function getResult(bytes32 name) public payable returns (uint256, int8) { - (uint256 result, int8 power) = transparentForwarder.getResult{ - value: msg.value - }(name); - // * Storing the result to test since values doesn't return in ethers for payable function + function updateResult( + bytes calldata data + ) public payable returns (uint256, int8, uint256) { + (uint256 result, int8 power, uint256 timestamp) = transparentForwarder + .fetchResult{value: msg.value}(data); + lastResult = result; lastPower = power; - return (result, power); + lastTimestamp = timestamp; + return (result, power, timestamp); + } + + function getResult( + bytes32 name + ) public view returns (uint256, int8, uint256) { + (uint256 result, int8 power, uint256 timestamp) = transparentForwarder + .getResult(name); + return (result, power, timestamp); + } + + function validateResult( + bytes calldata data + ) public view returns (bool) { + return + transparentForwarder.validateResult( + data + ); } } diff --git a/gas-report.txt b/gas-report.txt new file mode 100644 index 0000000..7b71096 --- /dev/null +++ b/gas-report.txt @@ -0,0 +1,59 @@ +·----------------------------------------------------|----------------------------|-------------|-----------------------------· +| Solc version: 0.8.9 · Optimizer enabled: false · Runs: 200 · Block limit: 30000000 gas │ +·····················································|····························|·············|······························ +| Methods │ +·························|···························|··············|·············|·············|···············|·············· +| Contract · Method · Min · Max · Avg · # calls · usd (avg) │ +·························|···························|··············|·············|·············|···············|·············· +| Client · updateResult · 83239 · 235557 · 136493 · 7 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · grantRole · 102029 · 119129 · 111800 · 7 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · pause · - · - · 30376 · 3 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · revokeRole · - · - · 40837 · 3 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · setResultGetterSelector · - · - · 29279 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · setResultManager · - · - · 26771 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · setUpdateSelector · - · - · 29357 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · setValidateSelector · - · - · 46362 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Forwarder · unpause · - · - · 30330 · 3 · - │ +·························|···························|··············|·············|·············|···············|·············· +| ResultManager · grantRole · 102029 · 119129 · 114851 · 4 · - │ +·························|···························|··············|·············|·············|···············|·············· +| ResultManager · updateResult · 41081 · 132476 · 91128 · 8 · - │ +·························|···························|··············|·············|·············|···············|·············· +| ResultManager · updateSignerAddress · - · - · 31619 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · disableWhitelist · 24040 · 26040 · 24540 · 4 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · enableWhitelist · - · - · 45896 · 3 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · grantRole · - · - · 119062 · 4 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · revokeRole · - · - · 40855 · 1 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · setPermission · - · - · 120976 · 1 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Staking · withdraw · - · - · 30692 · 2 · - │ +·························|···························|··············|·············|·············|···············|·············· +| TransparentForwarder · grantRole · - · - · 119095 · 1 · - │ +·························|···························|··············|·············|·············|···············|·············· +| TransparentForwarder · setStaking · - · - · 46598 · 1 · - │ +·························|···························|··············|·············|·············|···············|·············· +| Deployments · · % of limit · │ +·····················································|··············|·············|·············|···············|·············· +| Client · - · - · 626958 · 2.1 % · - │ +·····················································|··············|·············|·············|···············|·············· +| Forwarder · - · - · 2354354 · 7.8 % · - │ +·····················································|··············|·············|·············|···············|·············· +| ResultManager · - · - · 2355573 · 7.9 % · - │ +·····················································|··············|·············|·············|···············|·············· +| Staking · - · - · 1573968 · 5.2 % · - │ +·····················································|··············|·············|·············|···············|·············· +| TransparentForwarder · - · - · 1609818 · 5.4 % · - │ +·----------------------------------------------------|--------------|-------------|-------------|---------------|-------------· \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2cb7d36..ad59cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "hardhat-project", "dependencies": { - "@openzeppelin/contracts": "^4.7.3", + "@openzeppelin/contracts": "^4.9.4", "@openzeppelin/contracts-upgradeable": "^4.7.3", "@openzeppelin/hardhat-upgrades": "^1.20.0", "axios": "^1.4.0", @@ -16,6 +16,7 @@ }, "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "@openzeppelin/merkle-tree": "^1.0.5", "hardhat": "^2.10.0", "hardhat-abi-exporter": "^2.10.0", "hardhat-gas-reporter": "^1.0.9" @@ -1307,9 +1308,9 @@ } }, "node_modules/@openzeppelin/contracts": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", - "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.4.tgz", + "integrity": "sha512-cZ47skkw2iUz7ApIkgin5bNff7GkCJCZev48HKp81+sRBE0So9yi+Nm5O9G2BMysbjSdA9o9dKDUx0J9Yy1LUQ==" }, "node_modules/@openzeppelin/contracts-upgradeable": { "version": "4.7.3", @@ -1405,6 +1406,477 @@ "node": ">=8" } }, + "node_modules/@openzeppelin/merkle-tree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.5.tgz", + "integrity": "sha512-JkwG2ysdHeIphrScNxYagPy6jZeNONgDRyqU6lbFgE8HKCZFSkcP8r6AjZs+3HZk4uRNV0kNBBzuWhKQ3YV7Kw==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "ethereum-cryptography": "^1.1.2" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/@openzeppelin/merkle-tree/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, "node_modules/@openzeppelin/upgrades-core": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.19.1.tgz", @@ -10209,9 +10681,9 @@ } }, "@openzeppelin/contracts": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", - "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.4.tgz", + "integrity": "sha512-cZ47skkw2iUz7ApIkgin5bNff7GkCJCZev48HKp81+sRBE0So9yi+Nm5O9G2BMysbjSdA9o9dKDUx0J9Yy1LUQ==" }, "@openzeppelin/contracts-upgradeable": { "version": "4.7.3", @@ -10274,6 +10746,275 @@ } } }, + "@openzeppelin/merkle-tree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.5.tgz", + "integrity": "sha512-JkwG2ysdHeIphrScNxYagPy6jZeNONgDRyqU6lbFgE8HKCZFSkcP8r6AjZs+3HZk4uRNV0kNBBzuWhKQ3YV7Kw==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.7.0", + "ethereum-cryptography": "^1.1.2" + }, + "dependencies": { + "@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "dev": true, + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "dev": true, + "requires": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "dev": true, + "requires": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "dev": true, + "requires": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "dev": true + }, + "@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "dev": true, + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "dev": true, + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "dev": true, + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "dev": true, + "requires": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true + }, + "@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true + }, + "@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "requires": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "requires": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "requires": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + } + } + }, "@openzeppelin/upgrades-core": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.19.1.tgz", diff --git a/package.json b/package.json index 82f2270..773e0c7 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "hardhat-project", "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "@openzeppelin/merkle-tree": "^1.0.5", "hardhat": "^2.10.0", "hardhat-abi-exporter": "^2.10.0", "hardhat-gas-reporter": "^1.0.9" @@ -11,7 +12,7 @@ "gasCost": "npx hardhat test && node gasCost.js" }, "dependencies": { - "@openzeppelin/contracts": "^4.7.3", + "@openzeppelin/contracts": "^4.9.4", "@openzeppelin/contracts-upgradeable": "^4.7.3", "@openzeppelin/hardhat-upgrades": "^1.20.0", "axios": "^1.4.0", diff --git a/test/Forwarder.js b/test/Forwarder.js index f677f89..2638bcc 100644 --- a/test/Forwarder.js +++ b/test/Forwarder.js @@ -1,9 +1,11 @@ const hre = require("hardhat"); const { expect } = require("chai"); +const { generateTree, getProof } = require("./helpers/tree"); const ids = [1, 2, 3, 4, 5]; -const result = [12122, 212121, 21212, 212, 21]; +let result = [12122, 212121, 21212, 212, 21]; const power = [2, 2, 2, 2, 5]; +let timestamp = [1620000000, 1620000000, 1620000000, 1620000000, 1620000000]; const namesHash = [ "0x1bbf634c3ad0a99dd58667a617f7773ccb7f37901afa8e9ea1e32212bddb83c9", @@ -15,51 +17,7 @@ const namesHash = [ const abiCoder = new hre.ethers.utils.AbiCoder(); -const getValues = () => { - let values = []; - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - values.push({ - power: power[i], - collectionId: id, - name: namesHash[i], - value: result[i], - }); - } - return values; -}; - -const getMessage = (epoch) => { - const timestampBN = hre.ethers.BigNumber.from("1231231"); - const values = getValues(); - const message = abiCoder.encode( - [ - "uint32", - "uint256", - "tuple[](int8 power, uint16 collectionId, bytes32 name, uint256 value)", - ], - [epoch, timestampBN, values] - ); - return message; -}; - -const getSignature = async (message, signer) => { - const messageHash = hre.ethers.utils.arrayify( - hre.ethers.utils.keccak256(message) - ); - - const signature = await signer.signMessage(messageHash); - return signature; -}; - -const getBlock = async (signer, epoch) => { - const message = getMessage(epoch); - const signature = await getSignature(message, signer); - return { - signature, - message, - }; -}; +const tree = generateTree(power, ids, namesHash, result, timestamp); describe("Forwarder tests", () => { let resultManager; @@ -181,43 +139,286 @@ describe("Forwarder tests", () => { ).to.be.not.reverted; }); - it("setting resultManager address for non contract address should revert", async () => { - await expect( - forwarder.setResultManager(signers[1].address) - ).to.be.rejectedWith("Not a contract address"); - + it("client should not be able to getResult or validateResult if selectors are not set", async () => { await expect(forwarder.setResultManager(resultManager.address)).to.be.not .reverted; + + await expect(client.getResult(namesHash[0])).to.be.revertedWithCustomError( + forwarder, + "NoSelectorPresent" + ); + const [proof, resultDecoded, signature] = await getProof( + tree, + 3, + signers[0] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] // The data in the same order + ); + await expect(client.updateResult(combinedData)).to.be.revertedWithCustomError( + forwarder, + "NoSelectorPresent" + ); + await expect(client.validateResult(combinedData)).to.be.revertedWithCustomError( + forwarder, + "NoSelectorPresent" + ); }); - it("getResult call should revert for unassigned collection payload", async () => { - // * Whitlist client address + it("only FORWARDER_ADMIN_ROLE should be able to update selector setters", async () => { + const FORWARDER_ADMIN_ROLE = await forwarder.FORWARDER_ADMIN_ROLE(); + await forwarder.revokeRole(FORWARDER_ADMIN_ROLE, signers[0].address); + await expect(forwarder.setResultGetterSelector("0x98d2a0f3")).to.be + .reverted; + await expect(forwarder.setUpdateSelector("0x2d444fd5")).to.be.reverted; + await expect(forwarder.setValidateSelector("0x41417a9d")).to.be.reverted; - await expect(client.getResult(namesHash[2])).to.be.rejectedWith( - "Invalid collection name" - ); + await forwarder.grantRole(FORWARDER_ADMIN_ROLE, signers[0].address); + await expect(forwarder.setResultGetterSelector("0xadd4c784")).to.be.not + .reverted; + await expect(forwarder.setUpdateSelector("0x2d444fd5")).to.be.not + .reverted; + await expect(forwarder.setValidateSelector("0x41417a9d")).to.be.not + .reverted; }); it("Forwarder should return required result", async () => { - const block = await getBlock(signers[0], epoch); + const FORWARDER_ROLE = await resultManager.FORWARDER_ROLE(); + await resultManager.grantRole(FORWARDER_ROLE, signers[0].address); + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[0] + ); + + // * update result via directly resultManager + expect( + await resultManager.updateResult( + tree.root, + proof, + resultDecoded, + signature + ) + ).to.be.not.reverted; + + const Client = await hre.ethers.getContractFactory("Client"); + const forwarderInterface = Client.attach(transparentForwarder.address); + const result = await forwarderInterface.getResult(resultDecoded[2]); + }); - await resultManager.setBlock(block); + it("update result via client", async () => { + const TRANSPARENT_FORWARDER_ROLE = + await forwarder.TRANSPARENT_FORWARDER_ROLE(); + await forwarder.grantRole(TRANSPARENT_FORWARDER_ROLE, signers[0].address); + const [proof, resultDecoded, signature] = await getProof( + tree, + 3, + signers[0] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] // The data in the same order + ); - const funcSignatureHash = "0xadd4c784"; - const payload = hre.ethers.utils.hexConcat([ - funcSignatureHash, - namesHash[0], - ]); + expect(await client.updateResult(combinedData)).to.be.not.reverted; + const lastResult = await client.lastResult(); + const lastPower = await client.lastPower(); + const lastTimestamp = await client.lastTimestamp(); - await forwarder.setCollectionPayload(namesHash[0], payload); + expect(lastResult).to.be.equal(resultDecoded[3]); + expect(lastPower).to.be.equal(resultDecoded[0]); + expect(lastTimestamp).to.be.equal(resultDecoded[4]); - expect(await client.getResult(namesHash[0])).to.be.not.reverted; - const clientResult = await client.lastResult(); - const clientPower = await client.lastPower(); - expect(result[0]).to.be.equal(clientResult); - expect(power[0]).to.be.equal(clientPower); + const clientResult = await client.getResult(namesHash[2]) + expect(clientResult[0]).to.be.equal(lastResult); + expect(clientResult[1]).to.be.equal(lastPower); + expect(clientResult[2]).to.be.equal(lastTimestamp); + }); + + it("update result via client should revert for invalid signature", async () => { + const [proof, resultDecoded, signature] = await getProof( + tree, + 2, + signers[1] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] // The data in the same order + ); + + await expect(client.updateResult(combinedData)).to.be.revertedWithCustomError( + resultManager, + "InvalidSignature" + ); + }); + + it("update result via client should revert for invalid merkle proof", async () => { + const [, resultDecoded_4, signature_4] = await getProof( + tree, + 4, + signers[0] + ); + const [proof_5, , ] = await getProof( + tree, + 5, + signers[0] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof_5, resultDecoded_4, signature_4] // The data in the same order + ); + await expect(client.updateResult(combinedData)).to.be.revertedWithCustomError( + resultManager, + "InvalidMerkleProof" + ); }); + it("validate should return true for valid signature", async () => { + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[0] + ); + + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] + ); + + expect( + await client.validateResult(combinedData) + ).to.be.true; + }); + + it("validate should return false for invalid signature", async () => { + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[1] + ); + + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] + ); + + expect( + await client.validateResult(combinedData) + ).to.be.false; + }); + + it("validate should return false for invalid merkle proof", async () => { + const [, resultDecoded_4, signature_4] = await getProof( + tree, + 4, + signers[0] + ); + const [proof_5, , ] = await getProof( + tree, + 5, + signers[0] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof_5, resultDecoded_4, signature_4] + ); + + expect( + await client.validateResult(combinedData) + ).to.be.false; + }); + + it("client should be able to update result if timestamp is greater than previous result", async () => { + let timestampUpdated = [1620000001, 1620000000, 1620000000, 1620000000, 1620000000]; + let resultUpdated = [12123, 212121, 21212, 212, 21]; + let tree_2 = generateTree(power, ids, namesHash, resultUpdated, timestampUpdated); + const [updatedProof, updatedResultDecoded, updatedSignature] = await getProof( + tree_2, + 1, + signers[0] + ); + + const updatedCombinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree_2.root, updatedProof, updatedResultDecoded, updatedSignature] + ); + + let resultBefore = await client.getResult(updatedResultDecoded[2]); + + expect(await client.updateResult(updatedCombinedData)).to.be.not.reverted; + let clientResultAfterUpdation = await client.lastResult(); + let clientTimestampAfterUpdation = await client.lastTimestamp(); + + expect(resultBefore[0]).to.be.lessThan(clientResultAfterUpdation); + expect(resultBefore[2]).to.be.lessThan(clientTimestampAfterUpdation); + + const [staleProof, staleResultDecoded, staleSignature] = await getProof( + tree, + 1, + signers[0] + ); + + const staleCombinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], + [tree.root, staleProof, staleResultDecoded, staleSignature] + ); + + expect(await client.updateResult(staleCombinedData)).to.be.not.reverted; + let clientResultAfterStaleUpdation = await client.lastResult(); + let clientTimestampAfterStaleUpdation = await client.lastTimestamp(); + + expect(clientResultAfterUpdation).to.be.equal(clientResultAfterStaleUpdation); + expect(clientTimestampAfterUpdation).to.be.equal(clientTimestampAfterStaleUpdation); + + }); + + it("Account should be able to access if whitelist mode is disabled", async () => { await staking.disableWhitelist(); await expect(client.getResult(namesHash[0])).to.be.not.reverted; @@ -225,13 +426,13 @@ describe("Forwarder tests", () => { it("Non whitelisted account should not be able to getResult", async () => { await staking.enableWhitelist(); - await expect( - transparentForwarderAsForwarder.getResult(namesHash[0]) - ).to.be.revertedWith("Not whitelisted"); + await expect(client.getResult(namesHash[0])).to.be.revertedWith( + "Not whitelisted" + ); }); it("Caller should have TRANSPARENT_FORWARDER_ROLE role to getResult", async () => { - await expect(forwarder.connect(signers[1]).getResult(namesHash[0])).to.be + await expect(forwarder.connect(signers[1])["getResult(bytes32)"](namesHash[0])).to.be .reverted; }); @@ -244,7 +445,21 @@ describe("Forwarder tests", () => { it("Client should be able to transfer ether in getResult", async () => { const transferAmount = hre.ethers.utils.parseEther("1"); await staking.setPermission(client.address); - await client.getResult(namesHash[0], { + const [proof, resultDecoded, signature] = await getProof( + tree, + 3, + signers[0] + ); + const combinedData = hre.ethers.utils.defaultAbiCoder.encode( + [ + "bytes32", + "bytes32[]", + "tuple(int8, uint16, bytes32, uint256, uint256)", + "bytes", + ], // The types in order + [tree.root, proof, resultDecoded, signature] // The data in the same order + ); + await client.updateResult(combinedData, { value: transferAmount, }); @@ -309,4 +524,4 @@ describe("Forwarder tests", () => { await expect(client.getResult(namesHash[0])).to.be.not.reverted; }); }); -}); \ No newline at end of file +}); diff --git a/test/ResultManager.js b/test/ResultManager.js index 9ca6a03..c8963b1 100644 --- a/test/ResultManager.js +++ b/test/ResultManager.js @@ -1,9 +1,11 @@ const hre = require("hardhat"); const { expect } = require("chai"); +const { generateTree, getProof } = require("./helpers/tree"); const ids = [1, 2, 3, 4, 5]; -const result = [12122, 212121, 21212, 212, 21]; +let result = [12122, 212121, 21212, 212, 21]; const power = [2, 2, 2, 2, 5]; +let timestamp = [1620000000, 1620000000, 1620000000, 1620000000, 1620000000]; const namesHash = [ "0x1bbf634c3ad0a99dd58667a617f7773ccb7f37901afa8e9ea1e32212bddb83c9", @@ -13,53 +15,8 @@ const namesHash = [ "0x0f5e947b204a798dd86405ac2f21fed0d109e748bcd057b913eb87b6ffe07c3e", ]; -const abiCoder = new hre.ethers.utils.AbiCoder(); - -const getValues = () => { - let values = []; - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - values.push({ - power: power[i], - collectionId: id, - name: namesHash[i], - value: result[i], - }); - } - return values; -}; - -const getMessage = (epoch) => { - const timestampBN = hre.ethers.BigNumber.from("1231231"); - const values = getValues(); - const message = abiCoder.encode( - [ - "uint32", - "uint256", - "tuple[](int8 power, uint16 collectionId, bytes32 name, uint256 value)", - ], - [epoch, timestampBN, values] - ); - return message; -}; - -const getSignature = async (message, signer) => { - const messageHash = hre.ethers.utils.arrayify( - hre.ethers.utils.keccak256(message) - ); - - const signature = await signer.signMessage(messageHash); - return signature; -}; - -const getBlock = async (signer, epoch) => { - const message = getMessage(epoch); - const signature = await getSignature(message, signer); - return { - signature, - message, - }; -}; + +const tree = generateTree(power, ids, namesHash, result, timestamp); describe("Result Manager tests", async () => { let resultManager; @@ -115,29 +72,170 @@ describe("Result Manager tests", async () => { ).to.be.reverted; }); - it("publish result should fail if signature is invalid", async () => { - const block = await getBlock(signers[1], epoch); + it("validateResult should return true for valid result", async () => { + const FORWARDER_ROLE = await resultManager.FORWARDER_ROLE(); + await resultManager.grantRole(FORWARDER_ROLE, signers[0].address); + const merkleRoot = tree.root; + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[0] + ); + const result = await resultManager.validateResult( + merkleRoot, + proof, + resultDecoded, + signature + ); + expect(result).to.be.true; + }); - await expect(resultManager.setBlock(block)).to.be.revertedWith( - "invalid signature" + it("validateResult should revert for invalid signature", async () => { + const merkleRoot = tree.root; + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[1] ); + const result = await resultManager.validateResult( + merkleRoot, + proof, + resultDecoded, + signature + ) + await expect( + result + ).to.be.false; }); - it("getResult should be only accessed by forwarder", async () => { - await expect(resultManager.connect(signers[1]).getResult(namesHash[0])).be - .reverted; - const FORWARDER_ROLE = await resultManager.FORWARDER_ROLE(); - await resultManager.grantRole(FORWARDER_ROLE, signers[1].address); - await expect(resultManager.connect(signers[1]).getResult(namesHash[0])).not - .be.reverted; + it("validateResult should revert for invalid merkle proof", async () => { + const merkleRoot = tree.root; + const [, resultDecoded_4, signature_4] = await getProof( + tree, + 4, + signers[0] + ); + const [proof_5, , ] = await getProof( + tree, + 5, + signers[0] + ); + const result = await resultManager.validateResult( + merkleRoot, + proof_5, + resultDecoded_4, + signature_4 + ) + await expect( + result + ).to.be.false; }); - it("setBlock should revert for same epoch", async () => { - const block = await getBlock(signers[0], epoch); - await expect(resultManager.setBlock(block)).to.be.not.reverted; - await expect(resultManager.setBlock(block)).to.be.rejectedWith( - "epoch must be > latestEpoch" + + it("updateResult should update the collection result", async () => { + const [proof, resultDecoded, signature] = await getProof( + tree, + 1, + signers[0] ); - epoch++; + expect( + await resultManager.updateResult( + tree.root, + proof, + resultDecoded, + signature + ) + ).to.be.not.reverted; + + const colResult = await resultManager.getResult(resultDecoded[2]); + // * value + expect(colResult[0]).to.be.equal(resultDecoded[3]); + // * power + expect(colResult[1]).to.be.equal(resultDecoded[0]); + // * timestamp + expect(colResult[2]).to.be.equal(resultDecoded[4]); + }); + + it("updateResult should revert for invalid signature", async () => { + let [proof, resultDecoded, signature] = await getProof( + tree, + 4, + signers[1] + ); + // should revert with message as invalid signature + await expect( + resultManager.updateResult(tree.root, proof, resultDecoded, signature) + ).to.be.revertedWithCustomError( + resultManager, + "InvalidSignature" + ); + }); + + it("updateResult should revert for invalid merkle proof", async () => { + const [, resultDecoded_4, signature_4] = await getProof( + tree, + 4, + signers[0] + ); + const [proof_5, , ] = await getProof( + tree, + 5, + signers[0] + ); + // should revert with message as invalid merkle proof + await expect( + resultManager.updateResult(tree.root, proof_5, resultDecoded_4, signature_4) + ).to.be.revertedWithCustomError( + resultManager, + "InvalidMerkleProof" + ); + }); + + it("should update result if timestamp is greater than previous result", async () => { + let timestampUpdated = [1620000001, 1620000000, 1620000000, 1620000000, 1620000000]; + let resultUpdated = [12123, 212121, 21212, 212, 21]; + let tree_2 = generateTree(power, ids, namesHash, resultUpdated, timestampUpdated); + + const [updatedProof, updatedResultDecoded, updatedSignature] = await getProof( + tree_2, + 1, + signers[0] + ); + + let resultBefore = await resultManager.getResult(updatedResultDecoded[2]); + + expect( + await resultManager.updateResult( + tree_2.root, + updatedProof, + updatedResultDecoded, + updatedSignature + ) + ).to.be.not.reverted; + + let resultAfterUpdation = await resultManager.getResult(updatedResultDecoded[2]); + + expect(resultBefore[2]).to.be.lessThan(resultAfterUpdation[2]); + expect(resultBefore[0]).to.be.lessThan(resultAfterUpdation[0]); + + const [staleProof, staleResultDecoded, staleSignature] = await getProof( + tree, + 1, + signers[0] + ); + + expect( + await resultManager.updateResult( + tree.root, + staleProof, + staleResultDecoded, + staleSignature + ) + ).to.be.not.reverted; + + let resultAfterStaleUpdation = await resultManager.getResult(updatedResultDecoded[2]); + + expect(resultAfterUpdation[2]).to.be.equal(resultAfterStaleUpdation[2]); + expect(resultAfterUpdation[0]).to.be.equal(resultAfterStaleUpdation[0]); }); }); diff --git a/test/helpers/tree.js b/test/helpers/tree.js new file mode 100644 index 0000000..2582e35 --- /dev/null +++ b/test/helpers/tree.js @@ -0,0 +1,59 @@ +const { StandardMerkleTree } = require("@openzeppelin/merkle-tree"); +const fs = require("fs"); +const { ethers } = require("hardhat"); + +const abiCoder = new ethers.utils.AbiCoder(); + +const timestamp = Math.floor(Date.now() / 1000); +const timestampBN = ethers.BigNumber.from(timestamp); + +const getValues = (power, ids, namesHash, result, timestamp) => { + let values = []; + for (let i = 0; i < ids.length; i++) { + const value = ethers.BigNumber.from(result[i]); + const collectionResult = abiCoder.encode( + ["int8", "uint16", "bytes32", "uint256", "uint256"], + [power[i], ids[i], namesHash[i], value, timestamp[i]] + ); + values.push([collectionResult]); + } + return values; +}; + +const generateTree = (power, ids, namesHash, result, timestamp) => { + const values = getValues(power, ids, namesHash, result, timestamp); + const tree = StandardMerkleTree.of(values, ["bytes"]); + console.log("Merkle Root:", tree.root); + return tree; +}; + +const getProof = async (tree, id, signer) => { + for (const [i, v] of tree.entries()) { + const proof = tree.getProof(i); + const resultDecoded = abiCoder.decode( + [ + "int8 power", + "uint16 id", + "bytes32 nameHash", + "uint256 result", + "uint256 timestamp", + ], + v[0] + ); + if (resultDecoded.id === id) { + const messageHash = ethers.utils.keccak256( + ethers.utils.concat([tree.root, v[0]]) + ); + const signature = await signer.signMessage( + ethers.utils.arrayify(messageHash) + ); + const resultDecoded = abiCoder.decode( + ["int8", "uint16", "bytes32", "uint256", "uint256"], + v[0] + ); + return [proof, resultDecoded, signature]; + } + } +}; + +module.exports = { getValues, generateTree, getProof };