From 3ad90933f184d99563645aeba2ea3c65194dd849 Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Thu, 20 Jun 2019 18:38:53 -0700 Subject: [PATCH 1/2] Remove dynamic type-checking from the Oracle contracts --- contracts/DelFiPrice.sol | 38 ++----- contracts/Oracle.sol | 100 ------------------ contracts/OracleData.sol | 37 +++++++ contracts/OraclePriceData.sol | 60 +++++++++++ contracts/{View.sol => OracleView.sol} | 12 +-- tests/DelFiPriceTest.js | 16 ++- tests/OracleDataTest.js | 123 ++++++++++++++++++++++ tests/OracleTest.js | 136 ------------------------- tests/OracleViewTest.js | 9 ++ tests/ViewTest.js | 9 -- tsrc/test.ts | 2 +- 11 files changed, 251 insertions(+), 291 deletions(-) delete mode 100644 contracts/Oracle.sol create mode 100644 contracts/OracleData.sol create mode 100644 contracts/OraclePriceData.sol rename contracts/{View.sol => OracleView.sol} (75%) create mode 100644 tests/OracleDataTest.js delete mode 100644 tests/OracleTest.js create mode 100644 tests/OracleViewTest.js delete mode 100644 tests/ViewTest.js diff --git a/contracts/DelFiPrice.sol b/contracts/DelFiPrice.sol index c1b26603..99208f8d 100644 --- a/contracts/DelFiPrice.sol +++ b/contracts/DelFiPrice.sol @@ -1,14 +1,14 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./Oracle.sol"; -import "./View.sol"; +import "./OraclePriceData.sol"; +import "./OracleView.sol"; /** * @notice The DelFi Price Feed View * @author Compound Labs, Inc. */ -contract DelFiPrice is View { +contract DelFiPrice is OracleView { /** * @notice The mapping of medianized prices per symbol */ @@ -17,30 +17,9 @@ contract DelFiPrice is View { /** * @notice The amount of time a price remains valid for (included in median) */ - uint public constant expiration = 24 hours; + uint public constant expiration = 48 hours; - /** - * @notice The namespace contract which enforces type-checking of values - * @dev This View defines its own type specification for values. - */ - address public namespace = address(this); - - /** - * @notice The name of the type-checking method in the namespace contract - */ - string public constant name = "price"; - - /** - * @notice Official data type checker for DelFi prices - * @param key Symbol to get price of as a string - * @param value Price in USD * 1e18 as a uint - * @return Reverts if not a DelFi price - */ - function price(bytes calldata key, bytes calldata value) external pure { - (abi.decode(key, (string)), abi.decode(value, (uint))); - } - - constructor(Oracle oracle_, address[] memory sources_) public View(oracle_, sources_) {} + constructor(OraclePriceData data_, address[] memory sources_) public OracleView(data_, sources_) {} /** * @notice Primary entry point to post and recalculate prices @@ -54,7 +33,7 @@ contract DelFiPrice is View { // Post the messages, whatever they are for (uint i = 0; i < messages.length; i++) { - oracle.put(namespace, name, messages[i], signatures[i]); + OraclePriceData(address(data)).put(messages[i], signatures[i]); } // Recalculate the asset prices @@ -77,10 +56,9 @@ contract DelFiPrice is View { function medianPrice(string memory symbol, address[] memory sources_, uint expiration_) public view returns (uint median, uint count) { uint[] memory postedPrices = new uint[](sources_.length); for (uint i = 0; i < sources_.length; i++) { - bytes memory key = abi.encode(symbol); - (uint timestamp, bytes memory value) = oracle.get(namespace, name, sources_[i], key); + (uint timestamp, uint price) = OraclePriceData(address(data)).get(sources_[i], symbol); if (block.timestamp < timestamp + expiration_) { - postedPrices[count] = abi.decode(value, (uint)); + postedPrices[count] = price; count++; } } diff --git a/contracts/Oracle.sol b/contracts/Oracle.sol deleted file mode 100644 index 2979c2f3..00000000 --- a/contracts/Oracle.sol +++ /dev/null @@ -1,100 +0,0 @@ -pragma solidity ^0.5.9; -pragma experimental ABIEncoderV2; - -/** - * @title The Open Oracle Data Contract - * @author Compound Labs, Inc. - */ -contract Oracle { - /** - * @notice The fundamental unit of storage for a reporter source - */ - struct Datum { - uint timestamp; - bytes value; - } - - /** - * @notice The most recent authenticated data from all sources - * @dev Datum are partitioned by their type checker (namespace, name) to guarantee safety. - * This is private because dynamic mapping keys preclude auto-generated getters. - */ - mapping(address => mapping(string => mapping(address => mapping(bytes => Datum)))) private data; - - struct PutLocalVars { - address source; - - uint timestamp; - bytes[] pairs; - bytes key; - bytes value; - - bytes tsg; - bytes fun; - bool success; - } - - /** - * @notice Write a bunch of signed datum to the authenticated storage mapping - * @param namespace The contract address which defines the (key, value) type - * @param name The method name (in namespace) which checks the (key, value) type - * @param message The payload containing the timestamp, and (key, value) pairs - * @param signature The cryptographic signature of the message payload, authorizing the source to write - */ - function put(address namespace, string calldata name, bytes calldata message, bytes calldata signature) external { - PutLocalVars memory vars; - - // Recover the source address - vars.source = source(message, signature); - - // Decode all the data tuples - (vars.timestamp, vars.pairs) = abi.decode(message, (uint256, bytes[])); - for (uint256 j = 0; j < vars.pairs.length; j++) { - (vars.key, vars.value) = abi.decode(vars.pairs[j], (bytes, bytes)); - - // Only update if type check passes (does not revert) - vars.tsg = abi.encodePacked(name, "(bytes,bytes)"); - vars.fun = abi.encodeWithSignature(string(vars.tsg), vars.key, vars.value); - (vars.success, ) = namespace.call(vars.fun); - if (!vars.success) { - continue; - } - - // Only update if newer than stored, according to source - Datum storage prior = data[namespace][name][vars.source][vars.key]; - if (prior.timestamp >= vars.timestamp) { - continue; - } - - // Update storage - data[namespace][name][vars.source][vars.key] = Datum(vars.timestamp, vars.value); - } - } - - /** - * @notice Read a single key with a pre-defined type signature from an authenticated source - * @param namespace The contract address which defines the (key, value) type - * @param name The method name (in namespace) which checks the (key, value) type - * @param source The verifiable author of the data - * @param key The selector for the value to return - * @return The claimed Unix timestamp for the data and the encoded value (defaults to (0, 0x)) - */ - function get(address namespace, string calldata name, address source, bytes calldata key) external view returns (uint256, bytes memory) { - Datum storage datum = data[namespace][name][source][key]; - return (datum.timestamp, datum.value); - } - - /** - * @notice Recovers the source address which signed a message - * @dev Comparing to a claimed address would add nothing, - * as the caller could simply perform the recover and claim that address. - * @param message The data that was presumably signed - * @param signature The fingerprint of the data + private key - * @return The source address which signed the message, presumably - */ - function source(bytes memory message, bytes memory signature) public pure returns (address) { - (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); - bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); - return ecrecover(hash, v, r, s); - } -} diff --git a/contracts/OracleData.sol b/contracts/OracleData.sol new file mode 100644 index 00000000..8802adc9 --- /dev/null +++ b/contracts/OracleData.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +/** + * @title The Open Oracle Data Base Contract + * @author Compound Labs, Inc. + */ +contract OracleData { + /** + * @notice Write a bunch of signed datum to the authenticated storage mapping + * @param message The payload containing the timestamp, and (key, value) pairs + * @param signature The cryptographic signature of the message payload, authorizing the source to write + */ + //function put(bytes calldata message, bytes calldata signature) external; + + /** + * @notice Read a single key with a pre-defined type signature from an authenticated source + * @param source The verifiable author of the data + * @param key The selector for the value to return + * @return The claimed Unix timestamp for the data and the encoded value (defaults to (0, 0x)) + */ + //function get(address source, key) external view returns (uint, ); + + /** + * @notice Recovers the source address which signed a message + * @dev Comparing to a claimed address would add nothing, + * as the caller could simply perform the recover and claim that address. + * @param message The data that was presumably signed + * @param signature The fingerprint of the data + private key + * @return The source address which signed the message, presumably + */ + function source(bytes memory message, bytes memory signature) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = abi.decode(signature, (bytes32, bytes32, uint8)); + bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(message))); + return ecrecover(hash, v, r, s); + } +} diff --git a/contracts/OraclePriceData.sol b/contracts/OraclePriceData.sol new file mode 100644 index 00000000..683225c8 --- /dev/null +++ b/contracts/OraclePriceData.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "./OracleData.sol"; + +/** + * @title The Open Oracle Price Data Contract + * @author Compound Labs, Inc. + */ +contract OraclePriceData is OracleData { + /** + * @notice The fundamental unit of storage for a reporter source + */ + struct Datum { + uint timestamp; + uint value; + } + + /** + * @notice The most recent authenticated data from all sources + * @dev This is private because dynamic mapping keys preclude auto-generated getters. + */ + mapping(address => mapping(string => Datum)) private data; + + /** + * @notice Write a bunch of signed datum to the authenticated storage mapping + * @param message The payload containing the timestamp, and (key, value) pairs + * @param signature The cryptographic signature of the message payload, authorizing the source to write + */ + function put(bytes calldata message, bytes calldata signature) external { + // Recover the source address + address source = source(message, signature); + + // Decode all the data tuples + (uint timestamp, bytes[] memory pairs) = abi.decode(message, (uint, bytes[])); + for (uint j = 0; j < pairs.length; j++) { + (string memory key, uint value) = abi.decode(pairs[j], (string, uint)); + + // Only update if newer than stored, according to source + Datum storage prior = data[source][key]; + if (prior.timestamp >= timestamp) { + continue; + } + + // Update storage + data[source][key] = Datum(timestamp, value); + } + } + + /** + * @notice Read a single key from an authenticated source + * @param source The verifiable author of the data + * @param key The selector for the value to return + * @return The claimed Unix timestamp for the data and the encoded value (defaults to (0, 0)) + */ + function get(address source, string calldata key) external view returns (uint, uint) { + Datum storage datum = data[source][key]; + return (datum.timestamp, datum.value); + } +} diff --git a/contracts/View.sol b/contracts/OracleView.sol similarity index 75% rename from contracts/View.sol rename to contracts/OracleView.sol index 30288e6f..88215dc5 100644 --- a/contracts/View.sol +++ b/contracts/OracleView.sol @@ -1,17 +1,17 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./Oracle.sol"; +import "./OracleData.sol"; /** * @title The Open Oracle View Base Contract * @author Compound Labs, Inc. */ -contract View { +contract OracleView { /** * @notice The Oracle Data Contract backing this View */ - Oracle public oracle; + OracleData public data; /** * @notice The static list of sources used by this View @@ -23,11 +23,11 @@ contract View { /** * @notice Construct a view given the oracle backing address and the list of sources * @dev According to the protocol, Views must be immutable to be considered conforming. - * @param oracle_ The address of the oracle data contract which is backing the view + * @param data_ The address of the oracle data contract which is backing the view * @param sources_ The list of source addresses to include in the aggregate value */ - constructor(Oracle oracle_, address[] memory sources_) public { - oracle = oracle_; + constructor(OracleData data_, address[] memory sources_) public { + data = data_; sources = sources_; } } diff --git a/tests/DelFiPriceTest.js b/tests/DelFiPriceTest.js index 669db2c0..4291330c 100644 --- a/tests/DelFiPriceTest.js +++ b/tests/DelFiPriceTest.js @@ -2,8 +2,6 @@ describe('Oracle', () => { it('sanity checks the delfi price view', async () => { const { address, - bytes, - uint256, deploy, encode, sign, @@ -24,8 +22,8 @@ describe('Oracle', () => { '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf18', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf19' ].map(web3.eth.accounts.privateKeyToAccount); - const oracle = await deploy('Oracle', []); - const delfi = await deploy('DelFiPrice', [oracle.address, sources.map(a => a.address)]); + const priceData = await deploy('OraclePriceData', []); + const delfi = await deploy('DelFiPrice', [priceData.address, sources.map(a => a.address)]); const now = new Date - 0; // Reads a price of an asset that doesn't exist yet @@ -38,7 +36,7 @@ describe('Oracle', () => { message, signature, signatory - } = sign(encode(timestamp, prices.map(([symbol, price]) => [bytes(symbol), uint256(price)])), signers[i].privateKey); + } = sign(encode(timestamp, prices.map(([symbol, price]) => [symbol, price])), signers[i].privateKey); expect(signatory).toEqual(signers[i].address); messages.push(message); signatures.push(signature); @@ -54,7 +52,7 @@ describe('Oracle', () => { /** Posts nothing **/ const post0 = await postPrices(now, [], ['ETH']) - expect(post0.gasUsed).toBeLessThan(100000); + expect(post0.gasUsed).toBeLessThan(60000); expect(await getPrice('ETH')).numEquals(0); @@ -63,7 +61,7 @@ describe('Oracle', () => { const post1 = await postPrices(now, [ [['ETH', 257]] ], ['ETH']); - expect(post1.gasUsed).toBeLessThan(200000); + expect(post1.gasUsed).toBeLessThan(135000); expect(await getPrice('ETH')).numEquals(257); @@ -80,7 +78,7 @@ describe('Oracle', () => { ['ETH', 255] ] ], ['ETH']); - expect(post2.gasUsed).toBeLessThan(500000); + expect(post2.gasUsed).toBeLessThan(240000); expect(await getPrice('BTC')).numEquals(0); // not added to list of symbols to update expect(await getPrice('ETH')).numEquals(257); @@ -102,7 +100,7 @@ describe('Oracle', () => { ['ETH', 255] ] ], ['BTC', 'ETH']); - expect(post3a.gasUsed).toBeLessThan(500000); + expect(post3a.gasUsed).toBeLessThan(300000); expect(await getPrice('BTC')).numEquals(8500); expect(await getPrice('ETH')).numEquals(256); diff --git a/tests/OracleDataTest.js b/tests/OracleDataTest.js new file mode 100644 index 00000000..31143c4f --- /dev/null +++ b/tests/OracleDataTest.js @@ -0,0 +1,123 @@ + +describe('OracleData', () => { + // XXX describe cant be async with jest :( + // all things considered, havent found a nice way to do setup + it('sets up the oracle data and tests some stuff', async () => { + const { + account, + address, + bytes, + deploy, + encode, + sign, + web3 + } = saddle; // XXX this kinda sucks + + const privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; + const oracleData = await deploy('OracleData', [], {from: account}); + const priceData = await deploy('OraclePriceData', [], {from: account}); + + // gets default data + let { + 0: timestamp, + 1: value + } = await priceData.methods.get(address(0), 'ETH').call(); + expect(timestamp).numEquals(0); + expect(value).numEquals(0); + + const delfi = await deploy('DelFiPrice', [priceData.address, [account]]); + const now = new Date - 0; + + // succeeds with message (no pairs) + signature + const K = 'ETH', V = 7; + let { + message, + signature, + signatory + } = sign(encode(now, []), privateKey); + + // the source we recover in solidity should match + expect(await oracleData.methods.source(message, signature).call()).toEqual(signatory); + expect(await oracleData.methods.source(bytes('bad'), signature).call()).not.toEqual(signatory); + await expect(oracleData.methods.source(message, bytes('0xbad')).call()).rejects.toThrow('revert'); + + // writes nothing + const wrote0 = await priceData.methods.put(message, signature).send({from: account}); + expect(wrote0.gasUsed).toBeLessThan(40000); + + // reads nothing + ({ + 0: timestamp, + 1: value + } = await priceData.methods.get(signatory, K).call()); + expect(timestamp).numEquals(0); + expect(value).numEquals(0); + + // writes 1 pair + ({ + message, + signature, + signatory + } = sign(encode(now, [[K, V]]), privateKey)); + + const wrote1 = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); + expect(wrote1.gasUsed).toBeLessThan(80000); + + // reads 1 pair + ({ + 0: timestamp, + 1: value + } = await priceData.methods.get(signatory, K).call()); + expect(timestamp).numEquals(now); + expect(value).numEquals(V); + + // write fails with older timestamp + ({ + message, + signature, + signatory + } = sign(encode(now - 1, [[K, 6]]), privateKey)); + + await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); + + ({ + 0: timestamp, + 1: value + } = await priceData.methods.get(signatory, K).call()); + expect(timestamp).numEquals(now); + expect(value).numEquals(V); + + // writes 2 pairs + ({ + message, + signature, + signatory + } = sign(encode(now, [ + ['ABC', 100], + ['BTC', 9000], + ]), privateKey)); + + const wrote2a = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); + expect(wrote2a.gasUsed).toBeLessThan(125000); + + ({ + 0: timestamp, + 1: value + } = await priceData.methods.get(signatory, 'BTC').call()); + expect(timestamp).numEquals(now); + expect(value).numEquals(9000); + + ({ + message, + signature, + signatory + } = sign(encode(now + 1, [ + ['ABC', 100], + ['BTC', 9000], + ]), privateKey)); + + const wrote2b = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); + expect(wrote2b.gasUsed).toBeLessThan(65000); + + }, 30000); +}); diff --git a/tests/OracleTest.js b/tests/OracleTest.js deleted file mode 100644 index dbf265e0..00000000 --- a/tests/OracleTest.js +++ /dev/null @@ -1,136 +0,0 @@ - -describe('Oracle', () => { - // XXX describe cant be async with jest :( - // all things considered, havent found a nice way to do setup - it('sets up the oracle and tests some stuff', async () => { - const { - account, - address, - bytes, - uint256, - deploy, - encode, - sign, - web3 - } = saddle; // XXX this kinda sucks - - const privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; - const oracle = await deploy('Oracle', [], {from: account}); - - // gets default data - let { - 0: timestamp, - 1: value - } = await oracle.methods.get(address(0), 'price', address(0), bytes('ETH')).call(); - expect(timestamp).numEquals(0); - expect(value).toBeNull(); - - // reverts with invalid type checker and garbage input - await expect( - oracle.methods.put(address(0), 'price', bytes('garbage msg'), bytes('garbage sig')).send({from: account}) - ).rejects.toThrow('revert'); - - const delfi = await deploy('DelFiPrice', [oracle.address, [account]]); - const now = new Date - 0; - - // reverts with valid type check and garbage input - await expect( - oracle.methods.put(delfi.address, 'price', bytes('garbage msg'), bytes('garbage sig')).send({from: account}) - ).rejects.toThrow('revert'); - - // succeeds with a proper type checker + message (no pairs) + signature - const K = bytes('ETH'), V = uint256(7); - let { - message, - signature, - signatory - } = sign(encode(now, []), privateKey); - - // the source we recover in solidity should match - expect(await oracle.methods.source(message, signature).call()).toEqual(signatory); - expect(await oracle.methods.source(bytes('bad'), signature).call()).not.toEqual(signatory); - await expect(oracle.methods.source(message, bytes('0xbad')).call()).rejects.toThrow('revert'); - - // the type checker should validate - expect(await delfi.methods.price(K, V).call()).toBeNull(); - - // writes nothing - const wrote0 = await oracle.methods.put(delfi.address, 'price', message, signature).send({from: account}); - expect(wrote0.gasUsed).toBeLessThan(40000); - - // reads nothing - ({ - 0: timestamp, - 1: value - } = await oracle.methods.get(delfi.address, 'price', signatory, K).call()); - expect(timestamp).numEquals(0); - expect(value).toBeNull(); - - // writes 1 pair - ({ - message, - signature, - signatory - } = sign(encode(now, [[K, V]]), privateKey)); - - const wrote1 = await oracle.methods.put(delfi.address, 'price', message, signature).send({from: account, gas: 1000000}); - expect(wrote1.gasUsed).toBeLessThan(120000); - - // reads 1 pair - ({ - 0: timestamp, - 1: value - } = await oracle.methods.get(delfi.address, 'price', signatory, K).call()); - expect(timestamp).numEquals(now); - expect(value).toEqual(V); - - // write fails with older timestamp - ({ - message, - signature, - signatory - } = sign(encode(now - 1, [[K, uint256(6)]]), privateKey)); - - await oracle.methods.put(delfi.address, 'price', message, signature).send({from: account, gas: 1000000}); - - ({ - 0: timestamp, - 1: value - } = await oracle.methods.get(delfi.address, 'price', signatory, K).call()); - expect(timestamp).numEquals(now); - expect(value).toEqual(V); - - // writes 2 pairs - ({ - message, - signature, - signatory - } = sign(encode(now, [ - [bytes('ABC'), uint256(100)], - [bytes('BTC'), uint256(9000)], - ]), privateKey)); - - const wrote2a = await oracle.methods.put(delfi.address, 'price', message, signature).send({from: account, gas: 1000000}); - expect(wrote2a.gasUsed).toBeLessThan(200000); - - ({ - 0: timestamp, - 1: value - } = await oracle.methods.get(delfi.address, 'price', signatory, bytes('BTC')).call()); - expect(timestamp).numEquals(now); - expect(value).toEqual(uint256(9000)); - - ({ - message, - signature, - signatory - } = sign(encode(now + 1, [ - [bytes('ABC'), uint256(100)], - [bytes('BTC'), uint256(9000)], - ]), privateKey)); - - const wrote2b = await oracle.methods.put(delfi.address, 'price', message, signature).send({from: account, gas: 1000000}); - expect(wrote2b.gasUsed).toBeLessThan(100000); - - }, 30000); -}); diff --git a/tests/OracleViewTest.js b/tests/OracleViewTest.js new file mode 100644 index 00000000..a1520db7 --- /dev/null +++ b/tests/OracleViewTest.js @@ -0,0 +1,9 @@ + +describe('View', () => { + it('is a valid view', async () => { + const oracleData = await saddle.deploy('OracleData', []); + const oracleView = await saddle.deploy('OracleView', [oracleData.address, []]); + + expect(await oracleView.methods.data.call()).toEqual(oracleData.address); + }); +}); diff --git a/tests/ViewTest.js b/tests/ViewTest.js deleted file mode 100644 index ac146e4c..00000000 --- a/tests/ViewTest.js +++ /dev/null @@ -1,9 +0,0 @@ - -describe('View', () => { - it('is a valid view', async () => { - const oracle = await saddle.deploy('Oracle', []); - const view = await saddle.deploy('View', [oracle.address, []]); - - expect(await view.methods.oracle.call()).toEqual(oracle.address); - }); -}); diff --git a/tsrc/test.ts b/tsrc/test.ts index 944b00ae..feb4d05f 100644 --- a/tsrc/test.ts +++ b/tsrc/test.ts @@ -27,7 +27,7 @@ export async function configure(network = 'test') { // XXX shared lib? function encode(timestamp, pairs) { return web3.eth.abi.encodeParameters(['uint256', 'bytes[]'], [timestamp, pairs.map(([k, v]) => { - return web3.eth.abi.encodeParameters(['bytes', 'bytes'], [k, v]) + return web3.eth.abi.encodeParameters(['string', 'uint'], [k, v]) })]); } From 6e9123ca5003022f4d0d5f8f49ef3b38de806725 Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Fri, 21 Jun 2019 13:35:15 -0700 Subject: [PATCH 2/2] Emit the Write event and Oracle -> OpenOracle in contract names --- contracts/DelFiPrice.sol | 12 ++++++------ contracts/{OracleData.sol => OpenOracleData.sol} | 7 ++++++- .../{OraclePriceData.sol => OpenOraclePriceData.sol} | 10 ++++++++-- contracts/{OracleView.sol => OpenOracleView.sol} | 8 ++++---- tests/DelFiPriceTest.js | 8 ++++---- tests/{OracleDataTest.js => OpenOracleDataTest.js} | 12 ++++++------ tests/OpenOracleViewTest.js | 9 +++++++++ tests/OracleViewTest.js | 9 --------- 8 files changed, 43 insertions(+), 32 deletions(-) rename contracts/{OracleData.sol => OpenOracleData.sol} (89%) rename contracts/{OraclePriceData.sol => OpenOraclePriceData.sol} (86%) rename contracts/{OracleView.sol => OpenOracleView.sol} (84%) rename tests/{OracleDataTest.js => OpenOracleDataTest.js} (90%) create mode 100644 tests/OpenOracleViewTest.js delete mode 100644 tests/OracleViewTest.js diff --git a/contracts/DelFiPrice.sol b/contracts/DelFiPrice.sol index 99208f8d..292e2230 100644 --- a/contracts/DelFiPrice.sol +++ b/contracts/DelFiPrice.sol @@ -1,14 +1,14 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./OraclePriceData.sol"; -import "./OracleView.sol"; +import "./OpenOraclePriceData.sol"; +import "./OpenOracleView.sol"; /** * @notice The DelFi Price Feed View * @author Compound Labs, Inc. */ -contract DelFiPrice is OracleView { +contract DelFiPrice is OpenOracleView { /** * @notice The mapping of medianized prices per symbol */ @@ -19,7 +19,7 @@ contract DelFiPrice is OracleView { */ uint public constant expiration = 48 hours; - constructor(OraclePriceData data_, address[] memory sources_) public OracleView(data_, sources_) {} + constructor(OpenOraclePriceData data_, address[] memory sources_) public OpenOracleView(data_, sources_) {} /** * @notice Primary entry point to post and recalculate prices @@ -33,7 +33,7 @@ contract DelFiPrice is OracleView { // Post the messages, whatever they are for (uint i = 0; i < messages.length; i++) { - OraclePriceData(address(data)).put(messages[i], signatures[i]); + OpenOraclePriceData(address(data)).put(messages[i], signatures[i]); } // Recalculate the asset prices @@ -56,7 +56,7 @@ contract DelFiPrice is OracleView { function medianPrice(string memory symbol, address[] memory sources_, uint expiration_) public view returns (uint median, uint count) { uint[] memory postedPrices = new uint[](sources_.length); for (uint i = 0; i < sources_.length; i++) { - (uint timestamp, uint price) = OraclePriceData(address(data)).get(sources_[i], symbol); + (uint timestamp, uint price) = OpenOraclePriceData(address(data)).get(sources_[i], symbol); if (block.timestamp < timestamp + expiration_) { postedPrices[count] = price; count++; diff --git a/contracts/OracleData.sol b/contracts/OpenOracleData.sol similarity index 89% rename from contracts/OracleData.sol rename to contracts/OpenOracleData.sol index 8802adc9..9fa7fde6 100644 --- a/contracts/OracleData.sol +++ b/contracts/OpenOracleData.sol @@ -5,7 +5,12 @@ pragma experimental ABIEncoderV2; * @title The Open Oracle Data Base Contract * @author Compound Labs, Inc. */ -contract OracleData { +contract OpenOracleData { + /** + * @notice The event emitted when a source writes to its storage + */ + //event Write(address indexed source, indexed key, uint timestamp, value); + /** * @notice Write a bunch of signed datum to the authenticated storage mapping * @param message The payload containing the timestamp, and (key, value) pairs diff --git a/contracts/OraclePriceData.sol b/contracts/OpenOraclePriceData.sol similarity index 86% rename from contracts/OraclePriceData.sol rename to contracts/OpenOraclePriceData.sol index 683225c8..2436cdf0 100644 --- a/contracts/OraclePriceData.sol +++ b/contracts/OpenOraclePriceData.sol @@ -1,13 +1,18 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./OracleData.sol"; +import "./OpenOracleData.sol"; /** * @title The Open Oracle Price Data Contract * @author Compound Labs, Inc. */ -contract OraclePriceData is OracleData { +contract OpenOraclePriceData is OpenOracleData { + /** + * @notice The event emitted when a source writes to its storage + */ + event Write(address indexed source, string indexed key, uint timestamp, uint value); + /** * @notice The fundamental unit of storage for a reporter source */ @@ -44,6 +49,7 @@ contract OraclePriceData is OracleData { // Update storage data[source][key] = Datum(timestamp, value); + emit Write(source, key, timestamp, value); } } diff --git a/contracts/OracleView.sol b/contracts/OpenOracleView.sol similarity index 84% rename from contracts/OracleView.sol rename to contracts/OpenOracleView.sol index 88215dc5..4a9c19ec 100644 --- a/contracts/OracleView.sol +++ b/contracts/OpenOracleView.sol @@ -1,17 +1,17 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; -import "./OracleData.sol"; +import "./OpenOracleData.sol"; /** * @title The Open Oracle View Base Contract * @author Compound Labs, Inc. */ -contract OracleView { +contract OpenOracleView { /** * @notice The Oracle Data Contract backing this View */ - OracleData public data; + OpenOracleData public data; /** * @notice The static list of sources used by this View @@ -26,7 +26,7 @@ contract OracleView { * @param data_ The address of the oracle data contract which is backing the view * @param sources_ The list of source addresses to include in the aggregate value */ - constructor(OracleData data_, address[] memory sources_) public { + constructor(OpenOracleData data_, address[] memory sources_) public { data = data_; sources = sources_; } diff --git a/tests/DelFiPriceTest.js b/tests/DelFiPriceTest.js index 4291330c..ab79a3cf 100644 --- a/tests/DelFiPriceTest.js +++ b/tests/DelFiPriceTest.js @@ -1,4 +1,4 @@ -describe('Oracle', () => { +describe('DelFiPrice', () => { it('sanity checks the delfi price view', async () => { const { address, @@ -22,7 +22,7 @@ describe('Oracle', () => { '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf18', '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf19' ].map(web3.eth.accounts.privateKeyToAccount); - const priceData = await deploy('OraclePriceData', []); + const priceData = await deploy('OpenOraclePriceData', []); const delfi = await deploy('DelFiPrice', [priceData.address, sources.map(a => a.address)]); const now = new Date - 0; @@ -78,7 +78,7 @@ describe('Oracle', () => { ['ETH', 255] ] ], ['ETH']); - expect(post2.gasUsed).toBeLessThan(240000); + expect(post2.gasUsed).toBeLessThan(250000); expect(await getPrice('BTC')).numEquals(0); // not added to list of symbols to update expect(await getPrice('ETH')).numEquals(257); @@ -100,7 +100,7 @@ describe('Oracle', () => { ['ETH', 255] ] ], ['BTC', 'ETH']); - expect(post3a.gasUsed).toBeLessThan(300000); + expect(post3a.gasUsed).toBeLessThan(320000); expect(await getPrice('BTC')).numEquals(8500); expect(await getPrice('ETH')).numEquals(256); diff --git a/tests/OracleDataTest.js b/tests/OpenOracleDataTest.js similarity index 90% rename from tests/OracleDataTest.js rename to tests/OpenOracleDataTest.js index 31143c4f..5e30f645 100644 --- a/tests/OracleDataTest.js +++ b/tests/OpenOracleDataTest.js @@ -1,5 +1,5 @@ -describe('OracleData', () => { +describe('OpenOracleData', () => { // XXX describe cant be async with jest :( // all things considered, havent found a nice way to do setup it('sets up the oracle data and tests some stuff', async () => { @@ -14,8 +14,8 @@ describe('OracleData', () => { } = saddle; // XXX this kinda sucks const privateKey = '0x177ee777e72b8c042e05ef41d1db0f17f1fcb0e8150b37cfad6993e4373bdf10'; - const oracleData = await deploy('OracleData', [], {from: account}); - const priceData = await deploy('OraclePriceData', [], {from: account}); + const oracleData = await deploy('OpenOracleData', [], {from: account}); + const priceData = await deploy('OpenOraclePriceData', [], {from: account}); // gets default data let { @@ -61,7 +61,7 @@ describe('OracleData', () => { } = sign(encode(now, [[K, V]]), privateKey)); const wrote1 = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); - expect(wrote1.gasUsed).toBeLessThan(80000); + expect(wrote1.gasUsed).toBeLessThan(82000); // reads 1 pair ({ @@ -98,7 +98,7 @@ describe('OracleData', () => { ]), privateKey)); const wrote2a = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); - expect(wrote2a.gasUsed).toBeLessThan(125000); + expect(wrote2a.gasUsed).toBeLessThan(130000); ({ 0: timestamp, @@ -117,7 +117,7 @@ describe('OracleData', () => { ]), privateKey)); const wrote2b = await priceData.methods.put(message, signature).send({from: account, gas: 1000000}); - expect(wrote2b.gasUsed).toBeLessThan(65000); + expect(wrote2b.gasUsed).toBeLessThan(70000); }, 30000); }); diff --git a/tests/OpenOracleViewTest.js b/tests/OpenOracleViewTest.js new file mode 100644 index 00000000..baf26314 --- /dev/null +++ b/tests/OpenOracleViewTest.js @@ -0,0 +1,9 @@ + +describe('OpenOracleView', () => { + it('is a valid view', async () => { + const oracleData = await saddle.deploy('OpenOracleData', []); + const oracleView = await saddle.deploy('OpenOracleView', [oracleData.address, []]); + + expect(await oracleView.methods.data.call()).toEqual(oracleData.address); + }); +}); diff --git a/tests/OracleViewTest.js b/tests/OracleViewTest.js deleted file mode 100644 index a1520db7..00000000 --- a/tests/OracleViewTest.js +++ /dev/null @@ -1,9 +0,0 @@ - -describe('View', () => { - it('is a valid view', async () => { - const oracleData = await saddle.deploy('OracleData', []); - const oracleView = await saddle.deploy('OracleView', [oracleData.address, []]); - - expect(await oracleView.methods.data.call()).toEqual(oracleData.address); - }); -});