Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tellor hashi adapter #20

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/evm/contracts/adapters/Tellor/ITellor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;


interface ITellor {
/// @notice Retrieves the data corresponding to a given queryId before a specified timestamp
/// Specs of how query ids are generated can be found here: https://github.com/tellor-io/dataSpecs
/// @param _queryId The ID of the query for which data is requested
/// @param _timestamp The timestamp before which data is to be retrieved
/// @return _available Indicates whether data is available or not
/// @return _value The data retrieved for the query in bytes
/// @return _timestampRetrieved The timestamp when the data was submitted
function getDataBefore(
bytes32 _queryId,
uint256 _timestamp
) external view returns (bool _available, bytes memory _value, uint256 _timestampRetrieved);

}
46 changes: 46 additions & 0 deletions packages/evm/contracts/adapters/Tellor/TellorAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import { ITellor } from "./ITellor.sol";
import { BlockHashOracleAdapter } from "../BlockHashOracleAdapter.sol";


contract TellorAdapter is BlockHashOracleAdapter {
/// @notice Tellor has a standard of how to request/submit data to oracle, you can find that information
/// here: https://github.com/tellor-io/dataSpecs
ITellor public tellor;

error BlockHashNotAvailable();

constructor(address _tellorAddress) {
tellor = ITellor(_tellorAddress);
}

/// @notice Stores a single block hash for a single given block number.
/// @param chainId Network identifier for the chain on which the block was mined.
/// @param blockNumber Identifier for the block for which to set the header.
function storeHash(uint256 chainId, uint256 blockNumber) public {
bytes memory _queryData = abi.encode("EVMHeader", abi.encode(chainId, blockNumber));
bytes32 _queryId = keccak256(_queryData);
// delay 15 minutes to allow for disputes to be raised if bad value is submitted
// (the longer the delay the stronger the security)
(bool retrieved, bytes memory _hashValue, ) = tellor.getDataBefore(_queryId, block.timestamp - 15 minutes);
if (!retrieved) revert BlockHashNotAvailable();
_storeHash(chainId, blockNumber, bytes32(_hashValue));
}
/// @notice Stores the block hashes for a given array of block numbers.
/// @param chainId Network identifier for the chain on which the block was mined.
/// @param blockNumbers List of block identifiers for which to store block hashes.
function storeHashes(uint256 chainId, uint256[] calldata blockNumbers) public {
bytes memory _queryData = abi.encode("EVMHeaderslist", abi.encode(chainId, blockNumbers));
bytes32 _queryId = keccak256(_queryData);
// delay 15 minutes to allow for disputes to be raised if bad value is submitted
// (the longer the stronger the security)
(bool retrieved, bytes memory _hashValue, ) = tellor.getDataBefore(_queryId, block.timestamp - 15 minutes);
if (!retrieved) revert BlockHashNotAvailable();
bytes32[] memory _hashes = abi.decode(_hashValue, (bytes32[]));
for (uint256 i = 0; i < blockNumbers.length; i++) {
_storeHash(chainId, blockNumbers[i], _hashes[i]);
}
}
}
192 changes: 192 additions & 0 deletions packages/evm/contracts/adapters/Tellor/test/TellorPlayground.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract TellorPlayground {
// Storage
mapping(bytes32 => mapping(uint256 => bool)) public isDisputed; //queryId -> timestamp -> value
mapping(bytes32 => mapping(uint256 => address)) public reporterByTimestamp;
mapping(address => StakeInfo) public stakerDetails; //mapping from a persons address to their staking info
mapping(bytes32 => uint256[]) public timestamps;
mapping(bytes32 => mapping(uint256 => bytes)) public values; //queryId -> timestamp -> value
mapping(bytes32 => uint256[]) public voteRounds;

uint256 public voteCount;
// Structs
struct StakeInfo {
uint256 startDate; //stake start date
uint256 stakedBalance; // staked balance
uint256 lockedBalance; // amount locked for withdrawal
uint256 reporterLastTimestamp; // timestamp of reporter's last reported value
uint256 reportsSubmitted; // total number of reports submitted by reporter
}

// Functions
/**
* @dev A mock function to submit a value to be read without reporter staking needed
* @param _queryId the ID to associate the value to
* @param _value the value for the queryId
* @param _nonce the current value count for the query id
* @param _queryData the data used by reporters to fulfill the data query
*/
// slither-disable-next-line timestamp
function submitValue(bytes32 _queryId, bytes calldata _value, uint256 _nonce, bytes memory _queryData) external {
require(keccak256(_value) != keccak256(""), "value must be submitted");
require(_nonce == timestamps[_queryId].length || _nonce == 0, "nonce must match timestamp index");
require(_queryId == keccak256(_queryData) || uint256(_queryId) <= 100, "id must be hash of bytes data");
values[_queryId][block.timestamp] = _value;
timestamps[_queryId].push(block.timestamp);
reporterByTimestamp[_queryId][block.timestamp] = msg.sender;
stakerDetails[msg.sender].reporterLastTimestamp = block.timestamp;
stakerDetails[msg.sender].reportsSubmitted++;
}

/**
* @dev A mock function to create a dispute
* @param _queryId The tellorId to be disputed
* @param _timestamp the timestamp of the value to be disputed
*/
function beginDispute(bytes32 _queryId, uint256 _timestamp) external {
values[_queryId][_timestamp] = bytes("");
isDisputed[_queryId][_timestamp] = true;
voteCount++;
voteRounds[keccak256(abi.encodePacked(_queryId, _timestamp))].push(voteCount);
}

/**
* @dev Retrieves the latest value for the queryId before the specified timestamp
* @param _queryId is the queryId to look up the value for
* @param _timestamp before which to search for latest value
* @return _ifRetrieve bool true if able to retrieve a non-zero value
* @return _value the value retrieved
* @return _timestampRetrieved the value's timestamp
*/
function getDataBefore(
bytes32 _queryId,
uint256 _timestamp
) external view returns (bool _ifRetrieve, bytes memory _value, uint256 _timestampRetrieved) {
(bool _found, uint256 _index) = getIndexForDataBefore(_queryId, _timestamp);
if (!_found) return (false, bytes(""), 0);
_timestampRetrieved = getTimestampbyQueryIdandIndex(_queryId, _index);
_value = values[_queryId][_timestampRetrieved];
return (true, _value, _timestampRetrieved);
}

/**
* @dev Retrieves latest array index of data before the specified timestamp for the queryId
* @param _queryId is the queryId to look up the index for
* @param _timestamp is the timestamp before which to search for the latest index
* @return _found whether the index was found
* @return _index the latest index found before the specified timestamp
*/
// solhint-disable-next-line code-complexity
function getIndexForDataBefore(
bytes32 _queryId,
uint256 _timestamp
) public view returns (bool _found, uint256 _index) {
uint256 _count = getNewValueCountbyQueryId(_queryId);
if (_count > 0) {
uint256 _middle;
uint256 _start = 0;
uint256 _end = _count - 1;
uint256 _time;
//Checking Boundaries to short-circuit the algorithm
_time = getTimestampbyQueryIdandIndex(_queryId, _start);
if (_time >= _timestamp) return (false, 0);
_time = getTimestampbyQueryIdandIndex(_queryId, _end);
if (_time < _timestamp) {
while (isInDispute(_queryId, _time) && _end > 0) {
_end--;
_time = getTimestampbyQueryIdandIndex(_queryId, _end);
}
if (_end == 0 && isInDispute(_queryId, _time)) {
return (false, 0);
}
return (true, _end);
}
//Since the value is within our boundaries, do a binary search
while (true) {
_middle = (_end - _start) / 2 + 1 + _start;
_time = getTimestampbyQueryIdandIndex(_queryId, _middle);
if (_time < _timestamp) {
//get immediate next value
uint256 _nextTime = getTimestampbyQueryIdandIndex(_queryId, _middle + 1);
if (_nextTime >= _timestamp) {
if (!isInDispute(_queryId, _time)) {
// _time is correct
return (true, _middle);
} else {
// iterate backwards until we find a non-disputed value
while (isInDispute(_queryId, _time) && _middle > 0) {
_middle--;
_time = getTimestampbyQueryIdandIndex(_queryId, _middle);
}
if (_middle == 0 && isInDispute(_queryId, _time)) {
return (false, 0);
}
// _time is correct
return (true, _middle);
}
} else {
//look from middle + 1(next value) to end
_start = _middle + 1;
}
} else {
uint256 _prevTime = getTimestampbyQueryIdandIndex(_queryId, _middle - 1);
if (_prevTime < _timestamp) {
if (!isInDispute(_queryId, _prevTime)) {
// _prevTime is correct
return (true, _middle - 1);
} else {
// iterate backwards until we find a non-disputed value
_middle--;
while (isInDispute(_queryId, _prevTime) && _middle > 0) {
_middle--;
_prevTime = getTimestampbyQueryIdandIndex(_queryId, _middle);
}
if (_middle == 0 && isInDispute(_queryId, _prevTime)) {
return (false, 0);
}
// _prevtime is correct
return (true, _middle);
}
} else {
//look from start to middle -1(prev value)
_end = _middle - 1;
}
}
}
}
return (false, 0);
}

/**
* @dev Counts the number of values that have been submitted for a given ID
* @param _queryId the ID to look up
* @return uint256 count of the number of values received for the queryId
*/
function getNewValueCountbyQueryId(bytes32 _queryId) public view returns (uint256) {
return timestamps[_queryId].length;
}

/**
* @dev Gets the timestamp for the value based on their index
* @param _queryId is the queryId to look up
* @param _index is the value index to look up
* @return uint256 timestamp
*/
function getTimestampbyQueryIdandIndex(bytes32 _queryId, uint256 _index) public view returns (uint256) {
uint256 _len = timestamps[_queryId].length;
if (_len == 0 || _len <= _index) return 0;
return timestamps[_queryId][_index];
}

/**
* @dev Returns whether a given value is disputed
* @param _queryId unique ID of the data feed
* @param _timestamp timestamp of the value
* @return bool whether the value is disputed
*/
function isInDispute(bytes32 _queryId, uint256 _timestamp) public view returns (bool) {
return isDisputed[_queryId][_timestamp];
}
}
101 changes: 101 additions & 0 deletions packages/evm/test/adapters/Tellor/01_TELLORAdapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { expect } from "chai"
import { ethers, network } from "hardhat"

const CHAIN_ID = 1
const BLOCK_NUMBER_ONE = 123
const BLOCK_NUMBER_TWO = 456
const BLOCK_NUMBER_THREE = 789
const abiCoder = ethers.utils.defaultAbiCoder
const keccak256 = ethers.utils.keccak256
// Encoding data for the oracle according to tellor specs (see: https://github.com/tellor-io/dataSpecs)
let params = abiCoder.encode(["uint256", "uint256"], [CHAIN_ID, BLOCK_NUMBER_ONE])
let queryData = abiCoder.encode(["string", "bytes"], ["EVMHeader", params])
let queryId = keccak256(queryData)
const HASH_VALUE_ONE = "0x0000000000000000000000000000000000000000000000000000000000000001"
const HASH_VALUE_TWO = "0x0000000000000000000000000000000000000000000000000000000000000002"
const HASH_VALUE_THREE = "0x0000000000000000000000000000000000000000000000000000000000000003"

const setup = async () => {
await network.provider.request({ method: "hardhat_reset", params: [] })
const playground = await ethers.getContractFactory("TellorPlayground")
const tellorPlayground = await playground.deploy()
const TELLORAdapter = await ethers.getContractFactory("TellorAdapter")
const tellorAdapter = await TELLORAdapter.deploy(tellorPlayground.address)
return {
tellorPlayground,
tellorAdapter,
}
}

const advanceTimeByMinutes = async (minutes: number) => {
// Get the current block
const currentBlock = await ethers.provider.getBlock("latest")
// Calculate the time for the next block
const nextBlockTime = currentBlock.timestamp + minutes * 60 // increase by n minutes
// Advance time by sending a request directly to the node
await ethers.provider.send("evm_setNextBlockTimestamp", [nextBlockTime])
// Mine the next block for the time change to take effect
await ethers.provider.send("evm_mine", [])
}

describe("TELLORAdapter", () => {
describe("Constructor", () => {
it("Successfully deploy contract", async () => {
const { tellorPlayground, tellorAdapter } = await setup()
expect(await tellorAdapter.deployed())
expect(await tellorAdapter.tellor()).to.equal(tellorPlayground.address)
})
})

describe("StoreHash()", () => {
it("Stores hash", async () => {
const { tellorPlayground, tellorAdapter } = await setup()
// submit value to tellor oracle
await tellorPlayground.submitValue(queryId, HASH_VALUE_ONE, 0, queryData)
// fails if 15 minutes have not passed
await expect(tellorAdapter.storeHash(CHAIN_ID, BLOCK_NUMBER_ONE)).to.revertedWithCustomError(
tellorAdapter,
"BlockHashNotAvailable",
)
// advance time by 15 minutes to bypass security delay
await advanceTimeByMinutes(15)
// store hash
await tellorAdapter.storeHash(CHAIN_ID, BLOCK_NUMBER_ONE)
expect(await tellorAdapter.getHashFromOracle(CHAIN_ID, BLOCK_NUMBER_ONE)).to.equal(HASH_VALUE_ONE)
})
})

describe("StoreHashes()", () => {
it("Stores multiple hashes", async () => {
const { tellorPlayground, tellorAdapter } = await setup()
// submit value to tellor oracle
params = abiCoder.encode(["uint256", "uint256[]"], [CHAIN_ID, [BLOCK_NUMBER_ONE, BLOCK_NUMBER_TWO, BLOCK_NUMBER_THREE]])
queryData = abiCoder.encode(["string", "bytes"], ["EVMHeaderslist", params])
queryId = keccak256(queryData)
let value = abiCoder.encode(["bytes32[]"], [[HASH_VALUE_ONE, HASH_VALUE_TWO, HASH_VALUE_THREE]])
// tellor staked reporter submits value to tellor oracle
await tellorPlayground.submitValue(queryId, value, 0, queryData)
// requesting and storing hashes fails if 15 minutes have not passed since submission (security delay)
await expect(tellorAdapter.storeHashes(CHAIN_ID, [BLOCK_NUMBER_ONE, BLOCK_NUMBER_TWO, BLOCK_NUMBER_THREE])).to.revertedWithCustomError(
tellorAdapter,
"BlockHashNotAvailable",
)
// advance time by 15 minutes to bypass security delay
await advanceTimeByMinutes(15)
// store hash
await tellorAdapter.storeHashes(CHAIN_ID, [BLOCK_NUMBER_ONE, BLOCK_NUMBER_TWO, BLOCK_NUMBER_THREE])
expect(await tellorAdapter.getHashFromOracle(CHAIN_ID, BLOCK_NUMBER_ONE)).to.equal(HASH_VALUE_ONE)
expect(await tellorAdapter.getHashFromOracle(CHAIN_ID, BLOCK_NUMBER_TWO)).to.equal(HASH_VALUE_TWO)
expect(await tellorAdapter.getHashFromOracle(CHAIN_ID, BLOCK_NUMBER_THREE)).to.equal(HASH_VALUE_THREE)
})
})

describe("getHashFromOracle()", () => {
it("Returns 0 if no header is stored", async () => {
const { tellorAdapter } = await setup()
expect(await tellorAdapter.getHashFromOracle(CHAIN_ID, BLOCK_NUMBER_ONE)).to.equal(
"0x0000000000000000000000000000000000000000000000000000000000000000",
)
})
})
})