# Tutorial
In this tutorial we will learn about Oracles. Oracles are services that provide off-chain data to smart contracts on the Ethereum blockchain. This data can be used to trigger actions, create conditional contracts, and much more.
There are many oracle providers in the Ethereum ecosystem, each with their own set of features and pricing models. Some popular oracle providers include Chainlink, Band Protocol, and Provable. In this tutorial, we use a simplified version of Tellor, a decentralized oracle offering.
Tellor solves this problem by aligning the incentives of data reporters, data consumers, and Tellor token holders. In brief, anyone can deposit a stake and report data. For a period of time, anyone can pay a dispute fee to challenge any piece of data. Tellor stakeholders vote to determine the outcome of the dispute. If the data reporter loses the dispute, the reporter's stake goes to the disputing party. This creates a system where bad actors are punished and good actors are rewarded.
# Snapshot of Tellor implementation
We then use the oracle service inside Liquidator, a smart contract that allows us to exchange TokenX for TokenUSD, where user holds TokenUSD and the smart contract holds TokenUSD. The Liquidator queries the oracle for the current price (in USD) of TokenX and processes the exchange accordingly.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
import "./TellorPlayground.sol"; // https://github.com/tellor-io/TellorPlayground/blob/main/contracts/TellorPlayground.sol
contract SimpleTellor is TellorPlayground {
uint256 originTimestamp;
constructor() {
originTimestamp = block.timestamp;
initialize();
}
function initialize() private {
// Initialize TokenX price to 1 USD
uint256 _value = 1;
bytes32 _queryId = keccak256(bytes("TokenX"));
values[_queryId][block.timestamp] = abi.encodePacked(_value);
timestamps[_queryId].push(block.timestamp);
reporterByTimestamp[_queryId][block.timestamp] = msg.sender;
stakerDetails[msg.sender].reporterLastTimestamp = block.timestamp;
stakerDetails[msg.sender].reportsSubmitted++;
}
function getCurrentValue(bytes32 queryId)
external
view
returns (uint256 value)
{
uint256 currentTimestamp = block.timestamp;
// Retrieve the index of queryId where last update happened
(bool found, uint256 index) = getIndexForDataBefore(queryId, currentTimestamp + 1);
if (!found){revert();}
// Retrieve the timestamp of the last update of queryId
uint256 timestampRetrieved = getTimestampbyQueryIdandIndex(queryId, index);
bytes memory price;
if (timestampRetrieved == block.timestamp) {
bool _didGet;
(_didGet, price, ) = getDataBefore(queryId, currentTimestamp + 1);
if(!_didGet){revert();}
}
else { // Reporters reset the price
bool _didGet;
(_didGet, price, ) = getDataBefore(queryId, originTimestamp + 1);
if(!_didGet){revert();}
}
value = uint256(bytes32(price));
}
// function submitValue( // Copied from TellorPlayground for reference
// 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++;
// emit NewReport(
// _queryId,
// block.timestamp,
// _value,
// _nonce,
// _queryData,
// msg.sender
// );
// }
}
# Snapshot of liquidator exchange function
Despite security measures, oracles can be tricky to use, being the interface between real-world data and on-chain actions, they can sometimes report incorrect data, leading to incorrect transactions. In this case, Liquidator uses the latest reported values from SimpleTellor (our simplified version of Tellor).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
import 'OpenZeppelin/openzeppelin-contracts@4.4.2/contracts/token/ERC20/IERC20.sol';
interface ISimpleTellor {
function getCurrentValue(bytes32) external returns (uint256);
}
contract Liquidator {
bytes32 queryId;
address public player;
address public tokenX;
address public tokenUSD;
ISimpleTellor public priceFeedOracle;
constructor(
address _player,
address _tokenX,
address _tokenUSD,
address tellor
) {
player = _player;
tokenX = _tokenX;
tokenUSD = _tokenUSD;
queryId = keccak256(bytes("TokenX"));
priceFeedOracle = ISimpleTellor(tellor);
}
function completed() external view returns (bool) {
return IERC20(tokenUSD).balanceOf(player) == 10000;
}
function exchange(uint256 amountIn) external returns (bool success) {
require(
IERC20(tokenX).transferFrom(msg.sender, address(this), amountIn),
"transferFrom failed, is the bank approved?"
);
uint256 _value = priceFeedOracle.getCurrentValue(queryId);
require(_value != 0, "price of a token in USD cannot be 0");
uint256 amountOut = amountIn * _value;
require(
IERC20(tokenUSD).transfer(msg.sender, amountOut),
"transfer to reciepent failed"
);
success = true;
}
}
Can you manipulate the oracle to change the price and drain the Liquidator out of TokenUSD?
Note: You initially get 1 TokenX