-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #558 from lidofinance/feature/oracle-daemon-config
feat: add oracle daemon config contract
- Loading branch information
Showing
3 changed files
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> | ||
// 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 })) | ||
}) | ||
}) | ||
}) |