diff --git a/contracts/0.8.9/OracleDaemonConfig.sol b/contracts/0.8.9/OracleDaemonConfig.sol new file mode 100644 index 000000000..e2dc813f9 --- /dev/null +++ b/contracts/0.8.9/OracleDaemonConfig.sol @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.8.9; + +import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; + +contract OracleDaemonConfig is AccessControlEnumerable { + + bytes32 public constant CONFIG_MANAGER_ROLE = keccak256("CONFIG_MANAGER_ROLE"); + + mapping(string => bytes) private values; + + constructor(address _admin, address[] memory _configManagers) { + if (_admin == address(0)) revert ErrorZeroAddress(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + + for (uint256 i = 0; i < _configManagers.length; ) { + if (_configManagers[i] == address(0)) revert ErrorZeroAddress(); + _grantRole(CONFIG_MANAGER_ROLE, _configManagers[i]); + + unchecked { + ++i; + } + } + } + + function set(string calldata _key, bytes calldata _value) external onlyRole(CONFIG_MANAGER_ROLE) { + if (values[_key].length > 0) revert ErrorValueExists(_key); + values[_key] = _value; + + emit ConfigValueSet(_key, _value); + } + + function update(string calldata _key, bytes calldata _value) external onlyRole(CONFIG_MANAGER_ROLE) { + if (values[_key].length == 0) revert ErrorValueDoesntExist(_key); + values[_key] = _value; + + emit ConfigValueUpdated(_key, _value); + } + + function unset(string calldata _key) external onlyRole(CONFIG_MANAGER_ROLE) { + if (values[_key].length == 0) revert ErrorValueDoesntExist(_key); + delete values[_key]; + + emit ConfigValueUnset(_key); + } + + function get(string calldata _key) external view returns (bytes memory) { + if (values[_key].length == 0) revert ErrorValueDoesntExist(_key); + + return values[_key]; + } + + function getList(string[] calldata _keys) external view returns (bytes[] memory) { + bytes[] memory results = new bytes[](_keys.length); + + for (uint256 i = 0; i < _keys.length; ) { + if (values[_keys[i]].length == 0) revert ErrorValueDoesntExist(_keys[i]); + + results[i] = values[_keys[i]]; + + unchecked { + ++i; + } + } + + return results; + } + + error ErrorValueExists(string key); + error ErrorValueDoesntExist(string key); + error ErrorZeroAddress(); + + event ConfigValueSet(string key, bytes value); + event ConfigValueUpdated(string key, bytes value); + event ConfigValueUnset(string key); +} diff --git a/lib/abi/OracleDaemonConfig.json b/lib/abi/OracleDaemonConfig.json new file mode 100644 index 000000000..2f95a3bbb --- /dev/null +++ b/lib/abi/OracleDaemonConfig.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address[]","name":"_configManagers","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"string","name":"key","type":"string"}],"name":"ErrorValueDoesntExist","type":"error"},{"inputs":[{"internalType":"string","name":"key","type":"string"}],"name":"ErrorValueExists","type":"error"},{"inputs":[],"name":"ErrorZeroAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"bytes","name":"value","type":"bytes"}],"name":"ConfigValueSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"ConfigValueUnset","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"bytes","name":"value","type":"bytes"}],"name":"ConfigValueUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"inputs":[],"name":"CONFIG_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_key","type":"string"}],"name":"get","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string[]","name":"_keys","type":"string[]"}],"name":"getList","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_key","type":"string"},{"internalType":"bytes","name":"_value","type":"bytes"}],"name":"set","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_key","type":"string"}],"name":"unset","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_key","type":"string"},{"internalType":"bytes","name":"_value","type":"bytes"}],"name":"update","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/test/0.8.9/oracle-daemon-config.test.js b/test/0.8.9/oracle-daemon-config.test.js new file mode 100644 index 000000000..4025be7a6 --- /dev/null +++ b/test/0.8.9/oracle-daemon-config.test.js @@ -0,0 +1,128 @@ +const hre = require('hardhat') +const { keccak256 } = require('js-sha3') +const { ZERO_ADDRESS } = require('@aragon/contract-helpers-test') + +const { assert } = require('../helpers/assert') +const { EvmSnapshot } = require('../helpers/blockchain') + +const OracleDaemonConfig = hre.artifacts.require('OracleDaemonConfig.sol') + +contract('OracleDaemonConfig', async ([deployer, manager, stranger]) => { + let config, snapshot + const defaultKey = '12345' + const defaultValue = '0x'.padEnd(66, '0101') + const updatedDefaultValue = '0x'.padEnd(66, '0202') + + before(async () => { + config = await OracleDaemonConfig.new(deployer, [manager], { from: deployer }) + snapshot = new EvmSnapshot(hre.ethers.provider) + + await snapshot.make() + }) + + describe('happy path', async () => { + before(async () => { + await snapshot.rollback() + }) + + it('sets a value', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + }) + + it('gets a value', async () => { + const value = await config.get(defaultKey) + + assert.equal(defaultValue, value) + }) + + it('updates a value', async () => { + await config.update(defaultKey, updatedDefaultValue, { from: manager }) + + const value = await config.get(defaultKey) + + assert.notEqual(defaultValue, value) + assert.equal(updatedDefaultValue, value) + }) + + it('gets all values', async () => { + const values = await config.getList([defaultKey]) + + assert.equal(values.length, 1) + assert.deepEqual( + values, + [updatedDefaultValue] + ) + }) + + it('removes a value', async () => { + await config.unset(defaultKey, { from: manager }) + + assert.reverts(config.get(defaultKey)) + }) + + it('reverts while gets all values', async () => { + assert.reverts(config.getList([defaultKey]), `ErrorValueDoesntExist(${defaultKey})`) + }) + }) + + describe('edge cases', async () => { + beforeEach(async () => { + await snapshot.rollback() + }) + + it("reverts when defaultValue for update doesn't exist", async () => { + assert.reverts(config.update(defaultKey, defaultValue, { from: manager }), `ErrorValueDoesntExist(${defaultKey})`) + }) + + it("reverts when defaultValue for unset doen't exist", async () => { + assert.reverts(config.unset(defaultKey, { from: manager }), `ErrorValueDoesntExist(${defaultKey})`) + }) + + it('reverts when defaultValue for set already exists', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + assert.reverts(config.set(defaultKey, updatedDefaultValue, { from: manager }), `ErrorValueExists(${defaultKey})`) + }) + + it('reverts when admin is zero address', async () => { + assert.reverts(OracleDaemonConfig.new(ZERO_ADDRESS, [manager], { from: deployer }), 'ErrorZeroAddress()') + }) + + it('reverts when one of managers is zero address', async () => { + assert.reverts(OracleDaemonConfig.new(deployer, [manager, ZERO_ADDRESS], { from: deployer }), 'ErrorZeroAddress()') + }) + }) + + describe('access control', async () => { + beforeEach(async () => { + await snapshot.rollback() + }) + + it('stranger cannot set a defaultValue', async () => { + assert.reverts(config.set(defaultKey, defaultValue, { from: stranger })) + }) + + it('admin cannot set a defaultValue', async () => { + assert.reverts(config.set(defaultKey, defaultValue, { from: deployer })) + }) + + it('stranger cannot update a defaultValue', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + assert.reverts(config.update(defaultKey, updatedDefaultValue, { from: stranger })) + }) + + it('admin cannot update a defaultValue', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + assert.reverts(config.update(defaultKey, updatedDefaultValue, { from: deployer })) + }) + + it('stranger cannot unset a defaultValue', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + assert.reverts(config.unset(defaultKey, { from: stranger })) + }) + + it('stranger cannot unset a defaultValue', async () => { + await config.set(defaultKey, defaultValue, { from: manager }) + assert.reverts(config.unset(defaultKey, { from: deployer })) + }) + }) +})