-
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.
feat: add oracle daemon config contract
- Loading branch information
Showing
3 changed files
with
227 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,89 @@ | ||
// 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(string memory _key, bytes memory _value) external onlyRole(CONFIG_MANAGER_ROLE) { | ||
bytes32 keyHash = bytes32(keccak256(abi.encodePacked(_key))); | ||
require(_keyHashes.contains(keyHash), "VALUE_DOESNT_EXIST"); | ||
_values[keyHash] = _value; | ||
|
||
emit ConfigValueUpdated(keyHash, _key, _value); | ||
} | ||
|
||
function unset(string memory _key) external onlyRole(CONFIG_MANAGER_ROLE) { | ||
bytes32 keyHash = bytes32(keccak256(abi.encodePacked(_key))); | ||
require(_keyHashes.contains(keyHash), "VALUE_DOESNT_EXIST"); | ||
|
||
_keyHashes.remove(keyHash); | ||
delete _values[keyHash]; | ||
|
||
emit ConfigValueUnset(keyHash, _key); | ||
} | ||
|
||
function get(string memory _key) external view returns (Item memory value) { | ||
bytes32 keyHash = bytes32(keccak256(abi.encodePacked(_key))); | ||
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_, string key_, bytes value_); | ||
event ConfigValueUnset(bytes32 indexed keyHash_, 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"},{"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"},{"indexed":false,"internalType":"string","name":"key_","type":"string"}],"name":"ConfigValueUnset","type":"event"},{"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":"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":[{"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":"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"},{"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"}] |
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,137 @@ | ||
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(defaultKey) | ||
|
||
assert.equal(defaultValue, value) | ||
assert.equal(defaultKeyHash, keyHash) | ||
}) | ||
|
||
it('updates a value', async () => { | ||
await config.update(defaultKey, updatedDefaultValue, { from: manager }) | ||
|
||
const { keyHash, value } = await config.get(defaultKey) | ||
|
||
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(defaultKey, { from: manager }) | ||
|
||
assertRevert(config.get(defaultKey)) | ||
}) | ||
|
||
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 })) | ||
}) | ||
}) | ||
}) |