Skip to content

Commit

Permalink
Merge pull request #558 from lidofinance/feature/oracle-daemon-config
Browse files Browse the repository at this point in the history
feat: add oracle daemon config contract
  • Loading branch information
TheDZhon authored Feb 6, 2023
2 parents 73e8e3b + 1946c72 commit 1f1f5e1
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
80 changes: 80 additions & 0 deletions contracts/0.8.9/OracleDaemonConfig.sol
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);
}
1 change: 1 addition & 0 deletions lib/abi/OracleDaemonConfig.json
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"}]
128 changes: 128 additions & 0 deletions test/0.8.9/oracle-daemon-config.test.js
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 }))
})
})
})

0 comments on commit 1f1f5e1

Please sign in to comment.