pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./MixinResolver.sol"; import "./interfaces/IBinaryOptionMarket.sol"; // Libraries import "./SafeDecimalMath.sol"; // Internal references import "./BinaryOptionMarketManager.sol"; import "./BinaryOption.sol"; import "./interfaces/IExchangeRates.sol"; import "./interfaces/IERC20.sol"; import "./interfaces/IFeePool.sol"; // https://docs.synthetix.io/contracts/source/contracts/binaryoptionmarket contract BinaryOptionMarket is Owned, MixinResolver, IBinaryOptionMarket { /* ========== LIBRARIES ========== */ using SafeMath for uint; using SafeDecimalMath for uint; /* ========== TYPES ========== */ struct Options { BinaryOption long; BinaryOption short; } struct Prices { uint long; uint short; } struct Times { uint biddingEnd; uint maturity; uint expiry; } struct OracleDetails { bytes32 key; uint strikePrice; uint finalPrice; } /* ========== STATE VARIABLES ========== */ Options public options; Prices public prices; Times public times; OracleDetails public oracleDetails; BinaryOptionMarketManager.Fees public fees; BinaryOptionMarketManager.CreatorLimits public creatorLimits; // `deposited` tracks the sum of open bids on short and long, plus withheld refund fees. // This must explicitly be kept, in case tokens are transferred to the contract directly. uint public deposited; address public creator; bool public resolved; bool public refundsEnabled; uint internal _feeMultiplier; /* ---------- Address Resolver Configuration ---------- */ bytes32 internal constant CONTRACT_SYSTEMSTATUS = "SystemStatus"; bytes32 internal constant CONTRACT_EXRATES = "ExchangeRates"; bytes32 internal constant CONTRACT_SYNTHSUSD = "SynthsUSD"; bytes32 internal constant CONTRACT_FEEPOOL = "FeePool"; /* ========== CONSTRUCTOR ========== */ constructor( address _owner, address _creator, address _resolver, uint[2] memory _creatorLimits, // [capitalRequirement, skewLimit] bytes32 _oracleKey, uint _strikePrice, bool _refundsEnabled, uint[3] memory _times, // [biddingEnd, maturity, expiry] uint[2] memory _bids, // [longBid, shortBid] uint[3] memory _fees // [poolFee, creatorFee, refundFee] ) public Owned(_owner) MixinResolver(_resolver) { creator = _creator; creatorLimits = BinaryOptionMarketManager.CreatorLimits(_creatorLimits[0], _creatorLimits[1]); oracleDetails = OracleDetails(_oracleKey, _strikePrice, 0); times = Times(_times[0], _times[1], _times[2]); refundsEnabled = _refundsEnabled; (uint longBid, uint shortBid) = (_bids[0], _bids[1]); _checkCreatorLimits(longBid, shortBid); emit Bid(Side.Long, _creator, longBid); emit Bid(Side.Short, _creator, shortBid); // Note that the initial deposit of synths must be made by the manager, otherwise the contract's assumed // deposits will fall out of sync with its actual balance. Similarly the total system deposits must be updated in the manager. // A balance check isn't performed here since the manager doesn't know the address of the new contract until after it is created. uint initialDeposit = longBid.add(shortBid); deposited = initialDeposit; (uint poolFee, uint creatorFee) = (_fees[0], _fees[1]); fees = BinaryOptionMarketManager.Fees(poolFee, creatorFee, _fees[2]); _feeMultiplier = SafeDecimalMath.unit().sub(poolFee.add(creatorFee)); // Compute the prices now that the fees and deposits have been set. _updatePrices(longBid, shortBid, initialDeposit); // Instantiate the options themselves options.long = new BinaryOption(_creator, longBid); options.short = new BinaryOption(_creator, shortBid); } /* ========== VIEWS ========== */ function resolverAddressesRequired() public view returns (bytes32[] memory addresses) { addresses = new bytes32[](4); addresses[0] = CONTRACT_SYSTEMSTATUS; addresses[1] = CONTRACT_EXRATES; addresses[2] = CONTRACT_SYNTHSUSD; addresses[3] = CONTRACT_FEEPOOL; } /* ---------- External Contracts ---------- */ function _systemStatus() internal view returns (ISystemStatus) { return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS)); } function _exchangeRates() internal view returns (IExchangeRates) { return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES)); } function _sUSD() internal view returns (IERC20) { return IERC20(requireAndGetAddress(CONTRACT_SYNTHSUSD)); } function _feePool() internal view returns (IFeePool) { return IFeePool(requireAndGetAddress(CONTRACT_FEEPOOL)); } function _manager() internal view returns (BinaryOptionMarketManager) { return BinaryOptionMarketManager(owner); } /* ---------- Phases ---------- */ function _biddingEnded() internal view returns (bool) { return times.biddingEnd < now; } function _matured() internal view returns (bool) { return times.maturity < now; } function _expired() internal view returns (bool) { return resolved && (times.expiry < now || deposited == 0); } function phase() external view returns (Phase) { if (!_biddingEnded()) { return Phase.Bidding; } if (!_matured()) { return Phase.Trading; } if (!_expired()) { return Phase.Maturity; } return Phase.Expiry; } /* ---------- Market Resolution ---------- */ function _oraclePriceAndTimestamp() internal view returns (uint price, uint updatedAt) { return _exchangeRates().rateAndUpdatedTime(oracleDetails.key); } function oraclePriceAndTimestamp() external view returns (uint price, uint updatedAt) { return _oraclePriceAndTimestamp(); } function _isFreshPriceUpdateTime(uint timestamp) internal view returns (bool) { (uint maxOraclePriceAge, , ) = _manager().durations(); return (times.maturity.sub(maxOraclePriceAge)) <= timestamp; } function canResolve() external view returns (bool) { (, uint updatedAt) = _oraclePriceAndTimestamp(); return !resolved && _matured() && _isFreshPriceUpdateTime(updatedAt); } function _result() internal view returns (Side) { uint price; if (resolved) { price = oracleDetails.finalPrice; } else { (price, ) = _oraclePriceAndTimestamp(); } return oracleDetails.strikePrice <= price ? Side.Long : Side.Short; } function result() external view returns (Side) { return _result(); } /* ---------- Option Prices ---------- */ function _computePrices( uint longBids, uint shortBids, uint _deposited ) internal view returns (uint long, uint short) { require(longBids != 0 && shortBids != 0, "Bids must be nonzero"); uint optionsPerSide = _exercisableDeposits(_deposited); // The math library rounds up on an exact half-increment -- the price on one side may be an increment too high, // but this only implies a tiny extra quantity will go to fees. return (longBids.divideDecimalRound(optionsPerSide), shortBids.divideDecimalRound(optionsPerSide)); } function senderPriceAndExercisableDeposits() external view returns (uint price, uint exercisable) { // When the market is not yet resolved, both sides might be able to exercise all the options. // On the other hand, if the market has resolved, then only the winning side may exercise. exercisable = 0; if (!resolved || address(_option(_result())) == msg.sender) { exercisable = _exercisableDeposits(deposited); } // Send the correct price for each side of the market. if (msg.sender == address(options.long)) { price = prices.long; } else if (msg.sender == address(options.short)) { price = prices.short; } else { revert("Sender is not an option"); } } function pricesAfterBidOrRefund( Side side, uint value, bool refund ) external view returns (uint long, uint short) { (uint longTotalBids, uint shortTotalBids) = _totalBids(); // prettier-ignore function(uint, uint) pure returns (uint) operation = refund ? SafeMath.sub : SafeMath.add; if (side == Side.Long) { longTotalBids = operation(longTotalBids, value); } else { shortTotalBids = operation(shortTotalBids, value); } if (refund) { value = value.multiplyDecimalRound(SafeDecimalMath.unit().sub(fees.refundFee)); } return _computePrices(longTotalBids, shortTotalBids, operation(deposited, value)); } // Returns zero if the result would be negative. See the docs for the formulae this implements. function bidOrRefundForPrice( Side bidSide, Side priceSide, uint price, bool refund ) external view returns (uint) { uint adjustedPrice = price.multiplyDecimalRound(_feeMultiplier); uint bids = _option(priceSide).totalBids(); uint _deposited = deposited; uint unit = SafeDecimalMath.unit(); uint refundFeeMultiplier = unit.sub(fees.refundFee); if (bidSide == priceSide) { uint depositedByPrice = _deposited.multiplyDecimalRound(adjustedPrice); // For refunds, the numerator is the negative of the bid case and, // in the denominator the adjusted price has an extra factor of (1 - the refundFee). if (refund) { (depositedByPrice, bids) = (bids, depositedByPrice); adjustedPrice = adjustedPrice.multiplyDecimalRound(refundFeeMultiplier); } // The adjusted price is guaranteed to be less than 1: all its factors are also less than 1. return _subToZero(depositedByPrice, bids).divideDecimalRound(unit.sub(adjustedPrice)); } else { uint bidsPerPrice = bids.divideDecimalRound(adjustedPrice); // For refunds, the numerator is the negative of the bid case. if (refund) { (bidsPerPrice, _deposited) = (_deposited, bidsPerPrice); } uint value = _subToZero(bidsPerPrice, _deposited); return refund ? value.divideDecimalRound(refundFeeMultiplier) : value; } } /* ---------- Option Balances and Bids ---------- */ function _bidsOf(address account) internal view returns (uint long, uint short) { return (options.long.bidOf(account), options.short.bidOf(account)); } function bidsOf(address account) external view returns (uint long, uint short) { return _bidsOf(account); } function _totalBids() internal view returns (uint long, uint short) { return (options.long.totalBids(), options.short.totalBids()); } function totalBids() external view returns (uint long, uint short) { return _totalBids(); } function _claimableBalancesOf(address account) internal view returns (uint long, uint short) { return (options.long.claimableBalanceOf(account), options.short.claimableBalanceOf(account)); } function claimableBalancesOf(address account) external view returns (uint long, uint short) { return _claimableBalancesOf(account); } function totalClaimableSupplies() external view returns (uint long, uint short) { return (options.long.totalClaimableSupply(), options.short.totalClaimableSupply()); } function _balancesOf(address account) internal view returns (uint long, uint short) { return (options.long.balanceOf(account), options.short.balanceOf(account)); } function balancesOf(address account) external view returns (uint long, uint short) { return _balancesOf(account); } function totalSupplies() external view returns (uint long, uint short) { return (options.long.totalSupply(), options.short.totalSupply()); } function _exercisableDeposits(uint _deposited) internal view returns (uint) { // Fees are deducted at resolution, so remove them if we're still bidding or trading. return resolved ? _deposited : _deposited.multiplyDecimalRound(_feeMultiplier); } function exercisableDeposits() external view returns (uint) { return _exercisableDeposits(deposited); } /* ---------- Utilities ---------- */ function _chooseSide( Side side, uint longValue, uint shortValue ) internal pure returns (uint) { if (side == Side.Long) { return longValue; } return shortValue; } function _option(Side side) internal view returns (BinaryOption) { if (side == Side.Long) { return options.long; } return options.short; } // Returns zero if the result would be negative. function _subToZero(uint a, uint b) internal pure returns (uint) { return a < b ? 0 : a.sub(b); } function _checkCreatorLimits(uint longBid, uint shortBid) internal view { uint totalBid = longBid.add(shortBid); require(creatorLimits.capitalRequirement <= totalBid, "Insufficient capital"); uint skewLimit = creatorLimits.skewLimit; require( skewLimit <= longBid.divideDecimal(totalBid) && skewLimit <= shortBid.divideDecimal(totalBid), "Bids too skewed" ); } function _incrementDeposited(uint value) internal returns (uint _deposited) { _deposited = deposited.add(value); deposited = _deposited; _manager().incrementTotalDeposited(value); } function _decrementDeposited(uint value) internal returns (uint _deposited) { _deposited = deposited.sub(value); deposited = _deposited; _manager().decrementTotalDeposited(value); } function _requireManagerNotPaused() internal view { require(!_manager().paused(), "This action cannot be performed while the contract is paused"); } function requireActiveAndUnpaused() external view { _systemStatus().requireSystemActive(); _requireManagerNotPaused(); } /* ========== MUTATIVE FUNCTIONS ========== */ /* ---------- Bidding and Refunding ---------- */ function _updatePrices( uint longBids, uint shortBids, uint _deposited ) internal { (uint256 longPrice, uint256 shortPrice) = _computePrices(longBids, shortBids, _deposited); prices = Prices(longPrice, shortPrice); emit PricesUpdated(longPrice, shortPrice); } function bid(Side side, uint value) external duringBidding { if (value == 0) { return; } _option(side).bid(msg.sender, value); emit Bid(side, msg.sender, value); uint _deposited = _incrementDeposited(value); _sUSD().transferFrom(msg.sender, address(this), value); (uint longTotalBids, uint shortTotalBids) = _totalBids(); _updatePrices(longTotalBids, shortTotalBids, _deposited); } function refund(Side side, uint value) external duringBidding returns (uint refundMinusFee) { require(refundsEnabled, "Refunds disabled"); if (value == 0) { return 0; } // Require the market creator to leave sufficient capital in the market. if (msg.sender == creator) { (uint thisBid, uint thatBid) = _bidsOf(msg.sender); if (side == Side.Short) { (thisBid, thatBid) = (thatBid, thisBid); } _checkCreatorLimits(thisBid.sub(value), thatBid); } // Safe subtraction here and in related contracts will fail if either the // total supply, deposits, or wallet balance are too small to support the refund. refundMinusFee = value.multiplyDecimalRound(SafeDecimalMath.unit().sub(fees.refundFee)); _option(side).refund(msg.sender, value); emit Refund(side, msg.sender, refundMinusFee, value.sub(refundMinusFee)); uint _deposited = _decrementDeposited(refundMinusFee); _sUSD().transfer(msg.sender, refundMinusFee); (uint longTotalBids, uint shortTotalBids) = _totalBids(); _updatePrices(longTotalBids, shortTotalBids, _deposited); } /* ---------- Market Resolution ---------- */ function resolve() external onlyOwner afterMaturity systemActive managerNotPaused { require(!resolved, "Market already resolved"); // We don't need to perform stale price checks, so long as the price was // last updated recently enough before the maturity date. (uint price, uint updatedAt) = _oraclePriceAndTimestamp(); require(_isFreshPriceUpdateTime(updatedAt), "Price is stale"); oracleDetails.finalPrice = price; resolved = true; // Now remit any collected fees. // Since the constructor enforces that creatorFee + poolFee < 1, the balance // in the contract will be sufficient to cover these transfers. IERC20 sUSD = _sUSD(); uint _deposited = deposited; uint poolFees = _deposited.multiplyDecimalRound(fees.poolFee); uint creatorFees = _deposited.multiplyDecimalRound(fees.creatorFee); _decrementDeposited(creatorFees.add(poolFees)); sUSD.transfer(_feePool().FEE_ADDRESS(), poolFees); sUSD.transfer(creator, creatorFees); emit MarketResolved(_result(), price, updatedAt, deposited, poolFees, creatorFees); } /* ---------- Claiming and Exercising Options ---------- */ function _claimOptions() internal systemActive managerNotPaused afterBidding returns (uint longClaimed, uint shortClaimed) { uint exercisable = _exercisableDeposits(deposited); Side outcome = _result(); bool _resolved = resolved; // Only claim options if we aren't resolved, and only claim the winning side. uint longOptions; uint shortOptions; if (!_resolved || outcome == Side.Long) { longOptions = options.long.claim(msg.sender, prices.long, exercisable); } if (!_resolved || outcome == Side.Short) { shortOptions = options.short.claim(msg.sender, prices.short, exercisable); } require(longOptions != 0 || shortOptions != 0, "Nothing to claim"); emit OptionsClaimed(msg.sender, longOptions, shortOptions); return (longOptions, shortOptions); } function claimOptions() external returns (uint longClaimed, uint shortClaimed) { return _claimOptions(); } function exerciseOptions() external returns (uint) { // The market must be resolved if it has not been. if (!resolved) { _manager().resolveMarket(address(this)); } // If there are options to be claimed, claim them and proceed. (uint claimableLong, uint claimableShort) = _claimableBalancesOf(msg.sender); if (claimableLong != 0 || claimableShort != 0) { _claimOptions(); } // If the account holds no options, revert. (uint longBalance, uint shortBalance) = _balancesOf(msg.sender); require(longBalance != 0 || shortBalance != 0, "Nothing to exercise"); // Each option only needs to be exercised if the account holds any of it. if (longBalance != 0) { options.long.exercise(msg.sender); } if (shortBalance != 0) { options.short.exercise(msg.sender); } // Only pay out the side that won. uint payout = _chooseSide(_result(), longBalance, shortBalance); emit OptionsExercised(msg.sender, payout); if (payout != 0) { _decrementDeposited(payout); _sUSD().transfer(msg.sender, payout); } return payout; } /* ---------- Market Expiry ---------- */ function _selfDestruct(address payable beneficiary) internal { uint _deposited = deposited; if (_deposited != 0) { _decrementDeposited(_deposited); } // Transfer the balance rather than the deposit value in case there are any synths left over // from direct transfers. IERC20 sUSD = _sUSD(); uint balance = sUSD.balanceOf(address(this)); if (balance != 0) { sUSD.transfer(beneficiary, balance); } // Destroy the option tokens before destroying the market itself. options.long.expire(beneficiary); options.short.expire(beneficiary); selfdestruct(beneficiary); } function cancel(address payable beneficiary) external onlyOwner duringBidding { (uint longTotalBids, uint shortTotalBids) = _totalBids(); (uint creatorLongBids, uint creatorShortBids) = _bidsOf(creator); bool cancellable = longTotalBids == creatorLongBids && shortTotalBids == creatorShortBids; require(cancellable, "Not cancellable"); _selfDestruct(beneficiary); } function expire(address payable beneficiary) external onlyOwner { require(_expired(), "Unexpired options remaining"); _selfDestruct(beneficiary); } /* ========== MODIFIERS ========== */ modifier duringBidding() { require(!_biddingEnded(), "Bidding inactive"); _; } modifier afterBidding() { require(_biddingEnded(), "Bidding incomplete"); _; } modifier afterMaturity() { require(_matured(), "Not yet mature"); _; } modifier systemActive() { _systemStatus().requireSystemActive(); _; } modifier managerNotPaused() { _requireManagerNotPaused(); _; } /* ========== EVENTS ========== */ event Bid(Side side, address indexed account, uint value); event Refund(Side side, address indexed account, uint value, uint fee); event PricesUpdated(uint longPrice, uint shortPrice); event MarketResolved( Side result, uint oraclePrice, uint oracleTimestamp, uint deposited, uint poolFees, uint creatorFees ); event OptionsClaimed(address indexed account, uint longOptions, uint shortOptions); event OptionsExercised(address indexed account, uint value); }