Skip to content

Commit

Permalink
Remove dynamic type-checking from the Oracle contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
jflatow committed Jun 21, 2019
1 parent 5a5ca41 commit 3ad9093
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 291 deletions.
38 changes: 8 additions & 30 deletions contracts/DelFiPrice.sol
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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++;
}
}
Expand Down
100 changes: 0 additions & 100 deletions contracts/Oracle.sol

This file was deleted.

37 changes: 37 additions & 0 deletions contracts/OracleData.sol
Original file line number Diff line number Diff line change
@@ -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> key) external view returns (uint, <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);
}
}
60 changes: 60 additions & 0 deletions contracts/OraclePriceData.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 6 additions & 6 deletions contracts/View.sol → contracts/OracleView.sol
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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_;
}
}
16 changes: 7 additions & 9 deletions tests/DelFiPriceTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ describe('Oracle', () => {
it('sanity checks the delfi price view', async () => {
const {
address,
bytes,
uint256,
deploy,
encode,
sign,
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);


Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 3ad9093

Please sign in to comment.