Skip to content

Commit

Permalink
feat: add oracle daemon config contract
Browse files Browse the repository at this point in the history
  • Loading branch information
rkolpakov committed Feb 6, 2023
1 parent 16eca72 commit 0ac222a
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
86 changes: 86 additions & 0 deletions contracts/0.8.9/OracleDaemonConfig.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;

import "@openzeppelin/contracts-v4.4/utils/structs/EnumerableSet.sol";
import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol";

struct Item {
bytes32 keyHash;
bytes value;
}

contract OracleDaemonConfig is AccessControlEnumerable {
using EnumerableSet for EnumerableSet.Bytes32Set;

bytes32 public constant CONFIG_MANAGER_ROLE = keccak256("CONFIG_MANAGER_ROLE");

mapping(bytes32 => bytes) private _values;
EnumerableSet.Bytes32Set private _keyHashes;

constructor(address _admin, address[] memory _configManagers) {
_grantRole(DEFAULT_ADMIN_ROLE, _admin);

for (uint256 i = 0; i < _configManagers.length; ) {
_grantRole(CONFIG_MANAGER_ROLE, _configManagers[i]);

unchecked {
++i;
}
}
}

function set(string memory _key, bytes memory _value) external onlyRole(CONFIG_MANAGER_ROLE) {
bytes32 keyHash = bytes32(keccak256(abi.encodePacked(_key)));
require(!_keyHashes.contains(keyHash), "VALUE_EXISTS");

_keyHashes.add(keyHash);
_values[keyHash] = _value;

emit ConfigValueSet(keyHash, _key, _value);
}

function update(bytes32 _key, bytes memory _value) external onlyRole(CONFIG_MANAGER_ROLE) {
require(_keyHashes.contains(_key), "VALUE_DOESNT_EXIST");
_values[_key] = _value;

emit ConfigValueUpdated(_key, _value);
}

function unset(bytes32 _key) external onlyRole(CONFIG_MANAGER_ROLE) {
require(_keyHashes.contains(_key), "VALUE_DOESNT_EXIST");

_keyHashes.remove(_key);
delete _values[_key];

emit ConfigValueUnset(_key);
}

function get(bytes32 _keyHash) external view returns (Item memory value) {
require(_keyHashes.contains(_keyHash), "VALUE_DOESNT_EXIST");

return Item({keyHash: _keyHash, value: _values[_keyHash]});
}

function values() external view returns (Item[] memory) {
bytes32[] memory keys = _keyHashes.values();
Item[] memory values = new Item[](keys.length);

for (uint256 i = 0; i < keys.length; ) {
values[i].keyHash = keys[i];
values[i].value = _values[keys[i]];

unchecked {
++i;
}
}

return values;
}

event ConfigValueSet(bytes32 indexed keyHash_, string key_, bytes value_);
event ConfigValueUpdated(bytes32 indexed keyHash_, bytes value_);
event ConfigValueUnset(bytes32 indexed keyHash_);
}
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"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"keyHash_","type":"bytes32"},{"indexed":false,"internalType":"string","name":"key_","type":"string"},{"indexed":false,"internalType":"bytes","name":"value_","type":"bytes"}],"name":"ConfigValueSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"keyHash_","type":"bytes32"}],"name":"ConfigValueUnset","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"keyHash_","type":"bytes32"},{"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":"bytes32","name":"_keyHash","type":"bytes32"}],"name":"get","outputs":[{"components":[{"internalType":"bytes32","name":"keyHash","type":"bytes32"},{"internalType":"bytes","name":"value","type":"bytes"}],"internalType":"struct Item","name":"value","type":"tuple"}],"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":"bytes32","name":"_key","type":"bytes32"}],"name":"unset","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"},{"internalType":"bytes","name":"_value","type":"bytes"}],"name":"update","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"values","outputs":[{"components":[{"internalType":"bytes32","name":"keyHash","type":"bytes32"},{"internalType":"bytes","name":"value","type":"bytes"}],"internalType":"struct Item[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"}]
126 changes: 126 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,126 @@
const { assert } = require('chai')
const hre = require('hardhat')
const { keccak256 } = require('js-sha3')

const { assertRevert } = require('../helpers/assertThrow')
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 defaultKeyHash = '0x' + keccak256(defaultKey)
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 { keyHash, value } = await config.get(defaultKeyHash)

assert.equal(defaultValue, value)
assert.equal(defaultKeyHash, keyHash)
})

it('updates a value', async () => {
await config.update(defaultKeyHash, updatedDefaultValue, { from: manager })

const { keyHash, value } = await config.get(defaultKeyHash)

assert.notEqual(defaultValue, value)
assert.equal(updatedDefaultValue, value)
assert.equal(defaultKeyHash, keyHash)
})

it('gets all values', async () => {
const values = await config.values()

assert.equal(values.length, 1)
assert.deepEqual(values.map(i => i.keyHash), [defaultKeyHash])
assert.deepEqual(values.map(i => i.value), [updatedDefaultValue])
})

it('removes a value', async () => {
await config.unset(defaultKeyHash, { from: manager })

assertRevert(config.get(defaultKeyHash))
})

it('gets all values (empty)', async () => {
const values = await config.values()

assert.equal(values.length, 0)
assert.deepEqual(values.map(i => i.keyHash), [])
assert.deepEqual(values.map(i => i.value), [])
})
})

describe('edge cases', async () => {
beforeEach(async () => {
await snapshot.rollback()
})

it("reverts when defaultValue for update doesn't exist", async () => {
assertRevert(config.update(defaultKeyHash, defaultValue, { from: manager }), 'VALUE_DOESNT_EXIST')
})

it("reverts when defaultValue for unset doen't exist", async () => {
assertRevert(config.unset(defaultKeyHash, { from: manager }), 'VALUE_DOESNT_EXIST')
})

it("reverts when defaultValue for set already exists", async () => {
await config.set(defaultKeyHash, defaultValue, { from: manager })
assertRevert(config.set(defaultKeyHash, updatedDefaultValue, { from: manager }), 'VALUE_EXISTS')
})
})

describe('access control', async () => {
beforeEach(async () => {
await snapshot.rollback()
})

it('stranger cannot set a defaultValue', async () => {
assertRevert(config.set(defaultKeyHash, defaultValue, { from: stranger }))
})

it('admin cannot set a defaultValue', async () => {
assertRevert(config.set(defaultKeyHash, defaultValue, { from: deployer }))
})

it('stranger cannot update a defaultValue', async () => {
await config.set(defaultKeyHash, defaultValue, { from: manager })
assertRevert(config.update(defaultKeyHash, updatedDefaultValue, { from: stranger }))
})

it('admin cannot update a defaultValue', async () => {
await config.set(defaultKeyHash, defaultValue, { from: manager })
assertRevert(config.update(defaultKeyHash, updatedDefaultValue, { from: deployer }))
})

it('stranger cannot unset a defaultValue', async () => {
await config.set(defaultKeyHash, defaultValue, { from: manager })
assertRevert(config.unset(defaultKeyHash, { from: stranger }))
})

it('stranger cannot unset a defaultValue', async () => {
await config.set(defaultKeyHash, defaultValue, { from: manager })
assertRevert(config.unset(defaultKeyHash, { from: deployer }))
})
})

})

0 comments on commit 0ac222a

Please sign in to comment.