// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "./interfaces/StoreInterface.sol"; import "./interfaces/OracleAncillaryInterface.sol"; import "./interfaces/FinderInterface.sol"; import "./interfaces/IdentifierWhitelistInterface.sol"; import "./interfaces/OptimisticOracleV2Interface.sol"; import "./Constants.sol"; import "./implementation/Testable.sol"; import "./implementation/Lockable.sol"; import "./implementation/FixedPoint.sol"; import "./implementation/AncillaryData.sol"; import "./implementation/AddressWhitelist.sol"; /** * @title Optimistic Requester. * @notice Optional interface that requesters can implement to receive callbacks. * @dev this contract does _not_ work with ERC777 collateral currencies or any others that call into the receiver on * transfer(). Using an ERC777 token would allow a user to maliciously grief other participants (while also losing * money themselves). */ interface OptimisticRequester { /** * @notice Callback for proposals. * @param identifier price identifier being requested. * @param timestamp timestamp of the price being requested. * @param ancillaryData ancillary data of the price being requested. */ function priceProposed( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external; /** * @notice Callback for disputes. * @param identifier price identifier being requested. * @param timestamp timestamp of the price being requested. * @param ancillaryData ancillary data of the price being requested. * @param refund refund received in the case that refundOnDispute was enabled. */ function priceDisputed( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, uint256 refund ) external; /** * @notice Callback for settlement. * @param identifier price identifier being requested. * @param timestamp timestamp of the price being requested. * @param ancillaryData ancillary data of the price being requested. * @param price price that was resolved by the escalation process. */ function priceSettled( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, int256 price ) external; } /** * @title Optimistic Oracle. * @notice Pre-DVM escalation contract that allows faster settlement. */ contract OptimisticOracleV2 is OptimisticOracleV2Interface, Testable, Lockable { using SafeMath for uint256; using SafeERC20 for IERC20; using Address for address; // Finder to provide addresses for DVM contracts. FinderInterface public override finder; // Default liveness value for all price requests. uint256 public override defaultLiveness; // This is effectively the extra ancillary data to add ",ooRequester:0000000000000000000000000000000000000000". uint256 private constant MAX_ADDED_ANCILLARY_DATA = 53; uint256 public constant OO_ANCILLARY_DATA_LIMIT = ancillaryBytesLimit - MAX_ADDED_ANCILLARY_DATA; int256 public constant TOO_EARLY_RESPONSE = type(int256).min; /** * @notice Constructor. * @param _liveness default liveness applied to each price request. * @param _finderAddress finder to use to get addresses of DVM contracts. * @param _timerAddress address of the timer contract. Should be 0x0 in prod. */ constructor( uint256 _liveness, address _finderAddress, address _timerAddress ) Testable(_timerAddress) { finder = FinderInterface(_finderAddress); _validateLiveness(_liveness); defaultLiveness = _liveness; } /** * @notice Requests a new price. * @param identifier price identifier being requested. * @param timestamp timestamp of the price being requested. * @param ancillaryData ancillary data representing additional args being passed with the price request. * @param currency ERC20 token used for payment of rewards and fees. Must be approved for use with the DVM. * @param reward reward offered to a successful proposer. Will be pulled from the caller. Note: this can be 0, * which could make sense if the contract requests and proposes the value in the same call or * provides its own reward system. * @return totalBond default bond (final fee) + final fee that the proposer and disputer will be required to pay. * This can be changed with a subsequent call to setBond(). */ function requestPrice( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, IERC20 currency, uint256 reward ) external override nonReentrant() returns (uint256 totalBond) { require(_getState(msg.sender, identifier, timestamp, ancillaryData) == State.Invalid, "requestPrice: Invalid"); require(_getIdentifierWhitelist().isIdentifierSupported(identifier), "Unsupported identifier"); require(_getCollateralWhitelist().isOnWhitelist(address(currency)), "Unsupported currency"); require(timestamp <= getCurrentTime(), "Timestamp in future"); // This ensures that the ancillary data is <= the OO limit, which is lower than the DVM limit because the // OO adds some data before sending to the DVM. require(ancillaryData.length <= OO_ANCILLARY_DATA_LIMIT, "Ancillary Data too long"); uint256 finalFee = _getStore().computeFinalFee(address(currency)).rawValue; requests[_getId(msg.sender, identifier, timestamp, ancillaryData)] = Request({ proposer: address(0), disputer: address(0), currency: currency, settled: false, requestSettings: RequestSettings({ eventBased: false, refundOnDispute: false, callbackOnPriceProposed: false, callbackOnPriceDisputed: false, callbackOnPriceSettled: false, bond: finalFee, customLiveness: 0 }), proposedPrice: 0, resolvedPrice: 0, expirationTime: 0, reward: reward, finalFee: finalFee }); if (reward > 0) { currency.safeTransferFrom(msg.sender, address(this), reward); } emit RequestPrice(msg.sender, identifier, timestamp, ancillaryData, address(currency), reward, finalFee); // This function returns the initial proposal bond for this request, which can be customized by calling // setBond() with the same identifier and timestamp. return finalFee.mul(2); } /** * @notice Set the proposal bond associated with a price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @param bond custom bond amount to set. * @return totalBond new bond + final fee that the proposer and disputer will be required to pay. This can be * changed again with a subsequent call to setBond(). */ function setBond( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, uint256 bond ) external override nonReentrant() returns (uint256 totalBond) { require(_getState(msg.sender, identifier, timestamp, ancillaryData) == State.Requested, "setBond: Requested"); Request storage request = _getRequest(msg.sender, identifier, timestamp, ancillaryData); request.requestSettings.bond = bond; // Total bond is the final fee + the newly set bond. return bond.add(request.finalFee); } /** * @notice Sets the request to refund the reward if the proposal is disputed. This can help to "hedge" the caller * in the event of a dispute-caused delay. Note: in the event of a dispute, the winner still receives the other's * bond, so there is still profit to be made even if the reward is refunded. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. */ function setRefundOnDispute( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external override nonReentrant() { require( _getState(msg.sender, identifier, timestamp, ancillaryData) == State.Requested, "setRefundOnDispute: Requested" ); _getRequest(msg.sender, identifier, timestamp, ancillaryData).requestSettings.refundOnDispute = true; } /** * @notice Sets a custom liveness value for the request. Liveness is the amount of time a proposal must wait before * being auto-resolved. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @param customLiveness new custom liveness. */ function setCustomLiveness( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, uint256 customLiveness ) external override nonReentrant() { require( _getState(msg.sender, identifier, timestamp, ancillaryData) == State.Requested, "setCustomLiveness: Requested" ); _validateLiveness(customLiveness); _getRequest(msg.sender, identifier, timestamp, ancillaryData).requestSettings.customLiveness = customLiveness; } /** * @notice Sets the request to be an "event-based" request. * @dev Calling this method has a few impacts on the request: * * 1. The timestamp at which the request is evaluated is the time of the proposal, not the timestamp associated * with the request. * * 2. The proposer cannot propose the "too early" value (TOO_EARLY_RESPONSE). This is to ensure that a proposer who * prematurely proposes a response loses their bond. * * 3. RefundoOnDispute is automatically set, meaning disputes trigger the reward to be automatically refunded to * the requesting contract. * * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. */ function setEventBased( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external override nonReentrant() { require( _getState(msg.sender, identifier, timestamp, ancillaryData) == State.Requested, "setEventBased: Requested" ); Request storage request = _getRequest(msg.sender, identifier, timestamp, ancillaryData); request.requestSettings.eventBased = true; request.requestSettings.refundOnDispute = true; } /** * @notice Sets which callbacks should be enabled for the request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @param callbackOnPriceProposed whether to enable the callback onPriceProposed. * @param callbackOnPriceDisputed whether to enable the callback onPriceDisputed. * @param callbackOnPriceSettled whether to enable the callback onPriceSettled. */ function setCallbacks( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, bool callbackOnPriceProposed, bool callbackOnPriceDisputed, bool callbackOnPriceSettled ) external override nonReentrant() { require( _getState(msg.sender, identifier, timestamp, ancillaryData) == State.Requested, "setCallbacks: Requested" ); Request storage request = _getRequest(msg.sender, identifier, timestamp, ancillaryData); request.requestSettings.callbackOnPriceProposed = callbackOnPriceProposed; request.requestSettings.callbackOnPriceDisputed = callbackOnPriceDisputed; request.requestSettings.callbackOnPriceSettled = callbackOnPriceSettled; } /** * @notice Proposes a price value on another address' behalf. Note: this address will receive any rewards that come * from this proposal. However, any bonds are pulled from the caller. * @param proposer address to set as the proposer. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @param proposedPrice price being proposed. * @return totalBond the amount that's pulled from the caller's wallet as a bond. The bond will be returned to * the proposer once settled if the proposal is correct. */ function proposePriceFor( address proposer, address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, int256 proposedPrice ) public override nonReentrant() returns (uint256 totalBond) { require(proposer != address(0), "proposer address must be non 0"); require( _getState(requester, identifier, timestamp, ancillaryData) == State.Requested, "proposePriceFor: Requested" ); Request storage request = _getRequest(requester, identifier, timestamp, ancillaryData); if (request.requestSettings.eventBased) require(proposedPrice != TOO_EARLY_RESPONSE, "Cannot propose 'too early'"); request.proposer = proposer; request.proposedPrice = proposedPrice; // If a custom liveness has been set, use it instead of the default. request.expirationTime = getCurrentTime().add( request.requestSettings.customLiveness != 0 ? request.requestSettings.customLiveness : defaultLiveness ); totalBond = request.requestSettings.bond.add(request.finalFee); if (totalBond > 0) request.currency.safeTransferFrom(msg.sender, address(this), totalBond); emit ProposePrice( requester, proposer, identifier, timestamp, ancillaryData, proposedPrice, request.expirationTime, address(request.currency) ); // End the re-entrancy guard early to allow the caller to potentially take OO-related actions inside this callback. _startReentrantGuardDisabled(); // Callback. if (address(requester).isContract() && request.requestSettings.callbackOnPriceProposed) OptimisticRequester(requester).priceProposed(identifier, timestamp, ancillaryData); _endReentrantGuardDisabled(); } /** * @notice Proposes a price value for an existing price request. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @param proposedPrice price being proposed. * @return totalBond the amount that's pulled from the proposer's wallet as a bond. The bond will be returned to * the proposer once settled if the proposal is correct. */ function proposePrice( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData, int256 proposedPrice ) external override returns (uint256 totalBond) { // Note: re-entrancy guard is done in the inner call. return proposePriceFor(msg.sender, requester, identifier, timestamp, ancillaryData, proposedPrice); } /** * @notice Disputes a price request with an active proposal on another address' behalf. Note: this address will * receive any rewards that come from this dispute. However, any bonds are pulled from the caller. * @param disputer address to set as the disputer. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return totalBond the amount that's pulled from the caller's wallet as a bond. The bond will be returned to * the disputer once settled if the dispute was valid (the proposal was incorrect). */ function disputePriceFor( address disputer, address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) public override nonReentrant() returns (uint256 totalBond) { require(disputer != address(0), "disputer address must be non 0"); require( _getState(requester, identifier, timestamp, ancillaryData) == State.Proposed, "disputePriceFor: Proposed" ); Request storage request = _getRequest(requester, identifier, timestamp, ancillaryData); request.disputer = disputer; uint256 finalFee = request.finalFee; uint256 bond = request.requestSettings.bond; totalBond = bond.add(finalFee); if (totalBond > 0) { request.currency.safeTransferFrom(msg.sender, address(this), totalBond); } StoreInterface store = _getStore(); // Along with the final fee, "burn" part of the loser's bond to ensure that a larger bond always makes it // proportionally more expensive to delay the resolution even if the proposer and disputer are the same // party. // The total fee is the burned bond and the final fee added together. uint256 totalFee = finalFee.add(_computeBurnedBond(request)); if (totalFee > 0) { request.currency.safeIncreaseAllowance(address(store), totalFee); _getStore().payOracleFeesErc20(address(request.currency), FixedPoint.Unsigned(totalFee)); } _getOracle().requestPrice( identifier, _getTimestampForDvmRequest(request, timestamp), _stampAncillaryData(ancillaryData, requester) ); // Compute refund. uint256 refund = 0; if (request.reward > 0 && request.requestSettings.refundOnDispute) { refund = request.reward; request.reward = 0; request.currency.safeTransfer(requester, refund); } emit DisputePrice( requester, request.proposer, disputer, identifier, timestamp, ancillaryData, request.proposedPrice ); // End the re-entrancy guard early to allow the caller to potentially re-request inside this callback. _startReentrantGuardDisabled(); // Callback. if (address(requester).isContract() && request.requestSettings.callbackOnPriceDisputed) OptimisticRequester(requester).priceDisputed(identifier, timestamp, ancillaryData, refund); _endReentrantGuardDisabled(); } /** * @notice Disputes a price value for an existing price request with an active proposal. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return totalBond the amount that's pulled from the disputer's wallet as a bond. The bond will be returned to * the disputer once settled if the dispute was valid (the proposal was incorrect). */ function disputePrice( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external override returns (uint256 totalBond) { // Note: re-entrancy guard is done in the inner call. return disputePriceFor(msg.sender, requester, identifier, timestamp, ancillaryData); } /** * @notice Retrieves a price that was previously requested by a caller. Reverts if the request is not settled * or settleable. Note: this method is not view so that this call may actually settle the price request if it * hasn't been settled. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return resolved price. */ function settleAndGetPrice( bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external override nonReentrant() returns (int256) { if (_getState(msg.sender, identifier, timestamp, ancillaryData) != State.Settled) { _settle(msg.sender, identifier, timestamp, ancillaryData); } return _getRequest(msg.sender, identifier, timestamp, ancillaryData).resolvedPrice; } /** * @notice Attempts to settle an outstanding price request. Will revert if it isn't settleable. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return payout the amount that the "winner" (proposer or disputer) receives on settlement. This amount includes * the returned bonds as well as additional rewards. */ function settle( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) external override nonReentrant() returns (uint256 payout) { return _settle(requester, identifier, timestamp, ancillaryData); } /** * @notice Gets the current data structure containing all information about a price request. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return the Request data structure. */ function getRequest( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) public view override nonReentrantView() returns (Request memory) { return _getRequest(requester, identifier, timestamp, ancillaryData); } /** * @notice Computes the current state of a price request. See the State enum for more details. * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return the State. */ function getState( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) public view override nonReentrantView() returns (State) { return _getState(requester, identifier, timestamp, ancillaryData); } /** * @notice Checks if a given request has resolved, expired or been settled (i.e the optimistic oracle has a price). * @param requester sender of the initial price request. * @param identifier price identifier to identify the existing request. * @param timestamp timestamp to identify the existing request. * @param ancillaryData ancillary data of the price being requested. * @return boolean indicating true if price exists and false if not. */ function hasPrice( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) public view override nonReentrantView() returns (bool) { State state = _getState(requester, identifier, timestamp, ancillaryData); return state == State.Settled || state == State.Resolved || state == State.Expired; } /** * @notice Generates stamped ancillary data in the format that it would be used in the case of a price dispute. * @param ancillaryData ancillary data of the price being requested. * @param requester sender of the initial price request. * @return the stamped ancillary bytes. */ function stampAncillaryData(bytes memory ancillaryData, address requester) public pure override returns (bytes memory) { return _stampAncillaryData(ancillaryData, requester); } function _getId( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) private pure returns (bytes32) { return keccak256(abi.encodePacked(requester, identifier, timestamp, ancillaryData)); } function _settle( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) private returns (uint256 payout) { State state = _getState(requester, identifier, timestamp, ancillaryData); // Set it to settled so this function can never be entered again. Request storage request = _getRequest(requester, identifier, timestamp, ancillaryData); request.settled = true; if (state == State.Expired) { // In the expiry case, just pay back the proposer's bond and final fee along with the reward. request.resolvedPrice = request.proposedPrice; payout = request.requestSettings.bond.add(request.finalFee).add(request.reward); request.currency.safeTransfer(request.proposer, payout); } else if (state == State.Resolved) { // In the Resolved case, pay either the disputer or the proposer the entire payout (+ bond and reward). request.resolvedPrice = _getOracle().getPrice( identifier, _getTimestampForDvmRequest(request, timestamp), _stampAncillaryData(ancillaryData, requester) ); bool disputeSuccess = request.resolvedPrice != request.proposedPrice; uint256 bond = request.requestSettings.bond; // Unburned portion of the loser's bond = 1 - burned bond. uint256 unburnedBond = bond.sub(_computeBurnedBond(request)); // Winner gets: // - Their bond back. // - The unburned portion of the loser's bond. // - Their final fee back. // - The request reward (if not already refunded -- if refunded, it will be set to 0). payout = bond.add(unburnedBond).add(request.finalFee).add(request.reward); request.currency.safeTransfer(disputeSuccess ? request.disputer : request.proposer, payout); } else revert("_settle: not settleable"); emit Settle( requester, request.proposer, request.disputer, identifier, timestamp, ancillaryData, request.resolvedPrice, payout ); // Temporarily disable the re-entrancy guard early to allow the caller to take an OO-related action inside this callback. _startReentrantGuardDisabled(); // Callback. if (address(requester).isContract() && request.requestSettings.callbackOnPriceSettled) OptimisticRequester(requester).priceSettled(identifier, timestamp, ancillaryData, request.resolvedPrice); _endReentrantGuardDisabled(); } function _getRequest( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) private view returns (Request storage) { return requests[_getId(requester, identifier, timestamp, ancillaryData)]; } function _computeBurnedBond(Request storage request) private view returns (uint256) { // burnedBond = floor(bond / 2) return request.requestSettings.bond.div(2); } function _validateLiveness(uint256 _liveness) private pure { require(_liveness < 5200 weeks, "Liveness too large"); require(_liveness > 0, "Liveness cannot be 0"); } function _getState( address requester, bytes32 identifier, uint256 timestamp, bytes memory ancillaryData ) internal view returns (State) { Request storage request = _getRequest(requester, identifier, timestamp, ancillaryData); if (address(request.currency) == address(0)) return State.Invalid; if (request.proposer == address(0)) return State.Requested; if (request.settled) return State.Settled; if (request.disputer == address(0)) return request.expirationTime <= getCurrentTime() ? State.Expired : State.Proposed; return _getOracle().hasPrice( identifier, _getTimestampForDvmRequest(request, timestamp), _stampAncillaryData(ancillaryData, requester) ) ? State.Resolved : State.Disputed; } function _getOracle() internal view returns (OracleAncillaryInterface) { return OracleAncillaryInterface(finder.getImplementationAddress(OracleInterfaces.Oracle)); } function _getCollateralWhitelist() internal view returns (AddressWhitelist) { return AddressWhitelist(finder.getImplementationAddress(OracleInterfaces.CollateralWhitelist)); } function _getStore() internal view returns (StoreInterface) { return StoreInterface(finder.getImplementationAddress(OracleInterfaces.Store)); } function _getIdentifierWhitelist() internal view returns (IdentifierWhitelistInterface) { return IdentifierWhitelistInterface(finder.getImplementationAddress(OracleInterfaces.IdentifierWhitelist)); } function _getTimestampForDvmRequest(Request storage request, uint256 requestTimestamp) internal view returns (uint256) { if (request.requestSettings.eventBased) { uint256 liveness = request.requestSettings.customLiveness != 0 ? request.requestSettings.customLiveness : defaultLiveness; return request.expirationTime.sub(liveness); } else { return requestTimestamp; } } /** * @dev We don't handle specifically the case where `ancillaryData` is not already readily translateable in utf8. * For those cases, we assume that the client will be able to strip out the utf8-translateable part of the * ancillary data that this contract stamps. */ function _stampAncillaryData(bytes memory ancillaryData, address requester) internal pure returns (bytes memory) { // Since this contract will be the one to formally submit DVM price requests, its useful for voters to know who // the original requester was. return AncillaryData.appendKeyValueAddress(ancillaryData, "ooRequester", requester); } function getCurrentTime() public view override(Testable, OptimisticOracleV2Interface) returns (uint256) { return Testable.getCurrentTime(); } }