pragma solidity ^0.5.16; // Inheritance import "./MixinFuturesMarketSettings.sol"; import "./interfaces/IFuturesMarketBaseTypes.sol"; // Libraries import "openzeppelin-solidity-2.3.0/contracts/math/SafeMath.sol"; import "./SignedSafeMath.sol"; import "./SignedSafeDecimalMath.sol"; import "./SafeDecimalMath.sol"; // Internal references import "./interfaces/IExchangeCircuitBreaker.sol"; import "./interfaces/IExchangeRates.sol"; import "./interfaces/IExchanger.sol"; import "./interfaces/ISystemStatus.sol"; import "./interfaces/IERC20.sol"; /* * Synthetic Futures * ================= * * Futures markets allow users leveraged exposure to an asset, long or short. * A user must post some margin in order to open a futures account, and profits/losses are * continually tallied against this margin. If a user's margin runs out, then their position is closed * by a liquidation keeper, which is rewarded with a flat fee extracted from the margin. * * The Synthetix debt pool is effectively the counterparty to each trade, so if a particular position * is in profit, then the debt pool pays by issuing sUSD into their margin account, * while if the position makes a loss then the debt pool burns sUSD from the margin, reducing the * debt load in the system. * * As the debt pool underwrites all positions, the debt-inflation risk to the system is proportional to the * long-short skew in the market. It is therefore in the interest of the system to reduce the skew. * To encourage the minimisation of the skew, each position is charged a funding rate, which increases with * the size of the skew. The funding rate is charged continuously, and positions on the heavier side of the * market are charged the current funding rate times the notional value of their position, while positions * on the lighter side are paid at the same rate to keep their positions open. * As the funding rate is the same (but negated) on both sides of the market, there is an excess quantity of * funding being charged, which is collected by the debt pool, and serves to reduce the system debt. * * To combat front-running, the system does not confirm a user's order until the next price is received from * the oracle. Therefore opening a position is a three stage procedure: depositing margin, submitting an order, * and waiting for that order to be confirmed. The last transaction is performed by a keeper, * once a price update is detected. * * The contract architecture is as follows: * * - FuturesMarket.sol: one of these exists per asset. Margin is maintained isolated per market. * * - FuturesMarketManager.sol: the manager keeps track of which markets exist, and is the main window between * futures markets and the rest of the system. It accumulates the total debt * over all markets, and issues and burns sUSD on each market's behalf. * * - FuturesMarketSettings.sol: Holds the settings for each market in the global FlexibleStorage instance used * by SystemSettings, and provides an interface to modify these values. Other than * the base asset, these settings determine the behaviour of each market. * See that contract for descriptions of the meanings of each setting. * * Each futures market and the manager operates behind a proxy, and for efficiency they communicate with one another * using their underlying implementations. * * Technical note: internal functions within the FuturesMarket contract assume the following: * * - prices passed into them are valid; * * - funding has already been recomputed up to the current time (hence unrecorded funding is nil); * * - the account being managed was not liquidated in the same transaction; */ interface IFuturesMarketManagerInternal { function issueSUSD(address account, uint amount) external; function burnSUSD(address account, uint amount) external returns (uint postReclamationAmount); function payFee(uint amount) external; } // https://docs.synthetix.io/contracts/source/contracts/FuturesMarket contract FuturesMarketBase is MixinFuturesMarketSettings, IFuturesMarketBaseTypes { /* ========== LIBRARIES ========== */ using SafeMath for uint; using SignedSafeMath for int; using SignedSafeDecimalMath for int; using SafeDecimalMath for uint; /* ========== CONSTANTS ========== */ // This is the same unit as used inside `SignedSafeDecimalMath`. int private constant _UNIT = int(10**uint(18)); //slither-disable-next-line naming-convention bytes32 internal constant sUSD = "sUSD"; /* ========== STATE VARIABLES ========== */ // The market identifier in the futures system (manager + settings). Multiple markets can co-exist // for the same asset in order to allow migrations. bytes32 public marketKey; // The asset being traded in this market. This should be a valid key into the ExchangeRates contract. bytes32 public baseAsset; // The total number of base units in long and short positions. uint128 public marketSize; /* * The net position in base units of the whole market. * When this is positive, longs outweigh shorts. When it is negative, shorts outweigh longs. */ int128 public marketSkew; /* * The funding sequence allows constant-time calculation of the funding owed to a given position. * Each entry in the sequence holds the net funding accumulated per base unit since the market was created. * Then to obtain the net funding over a particular interval, subtract the start point's sequence entry * from the end point's sequence entry. * Positions contain the funding sequence entry at the time they were confirmed; so to compute * the net funding on a given position, obtain from this sequence the net funding per base unit * since the position was confirmed and multiply it by the position size. */ uint32 public fundingLastRecomputed; int128[] public fundingSequence; /* * Each user's position. Multiple positions can always be merged, so each user has * only have one position at a time. */ mapping(address => Position) public positions; /* * This holds the value: sum_{p in positions}{p.margin - p.size * (p.lastPrice + fundingSequence[p.lastFundingIndex])} * Then marketSkew * (price + _nextFundingEntry()) + _entryDebtCorrection yields the total system debt, * which is equivalent to the sum of remaining margins in all positions. */ int128 internal _entryDebtCorrection; // This increments for each position; zero reflects a position that does not exist. uint64 internal _nextPositionId = 1; // Holds the revert message for each type of error. mapping(uint8 => string) internal _errorMessages; /* ---------- Address Resolver Configuration ---------- */ bytes32 internal constant CONTRACT_CIRCUIT_BREAKER = "ExchangeCircuitBreaker"; bytes32 internal constant CONTRACT_EXCHANGER = "Exchanger"; bytes32 internal constant CONTRACT_FUTURESMARKETMANAGER = "FuturesMarketManager"; bytes32 internal constant CONTRACT_FUTURESMARKETSETTINGS = "FuturesMarketSettings"; bytes32 internal constant CONTRACT_SYSTEMSTATUS = "SystemStatus"; // convenience struct for passing params between position modification helper functions struct TradeParams { int sizeDelta; uint price; uint takerFee; uint makerFee; bytes32 trackingCode; // optional tracking code for volume source fee sharing } /* ========== CONSTRUCTOR ========== */ constructor( address _resolver, bytes32 _baseAsset, bytes32 _marketKey ) public MixinFuturesMarketSettings(_resolver) { baseAsset = _baseAsset; marketKey = _marketKey; // Initialise the funding sequence with 0 initially accrued, so that the first usable funding index is 1. fundingSequence.push(0); // Set up the mapping between error codes and their revert messages. _errorMessages[uint8(Status.InvalidPrice)] = "Invalid price"; _errorMessages[uint8(Status.PriceOutOfBounds)] = "Price out of acceptable range"; _errorMessages[uint8(Status.CanLiquidate)] = "Position can be liquidated"; _errorMessages[uint8(Status.CannotLiquidate)] = "Position cannot be liquidated"; _errorMessages[uint8(Status.MaxMarketSizeExceeded)] = "Max market size exceeded"; _errorMessages[uint8(Status.MaxLeverageExceeded)] = "Max leverage exceeded"; _errorMessages[uint8(Status.InsufficientMargin)] = "Insufficient margin"; _errorMessages[uint8(Status.NotPermitted)] = "Not permitted by this address"; _errorMessages[uint8(Status.NilOrder)] = "Cannot submit empty order"; _errorMessages[uint8(Status.NoPositionOpen)] = "No position open"; _errorMessages[uint8(Status.PriceTooVolatile)] = "Price too volatile"; } /* ========== VIEWS ========== */ /* ---------- External Contracts ---------- */ function resolverAddressesRequired() public view returns (bytes32[] memory addresses) { bytes32[] memory existingAddresses = MixinFuturesMarketSettings.resolverAddressesRequired(); bytes32[] memory newAddresses = new bytes32[](5); newAddresses[0] = CONTRACT_EXCHANGER; newAddresses[1] = CONTRACT_CIRCUIT_BREAKER; newAddresses[2] = CONTRACT_FUTURESMARKETMANAGER; newAddresses[3] = CONTRACT_FUTURESMARKETSETTINGS; newAddresses[4] = CONTRACT_SYSTEMSTATUS; addresses = combineArrays(existingAddresses, newAddresses); } function _exchangeCircuitBreaker() internal view returns (IExchangeCircuitBreaker) { return IExchangeCircuitBreaker(requireAndGetAddress(CONTRACT_CIRCUIT_BREAKER)); } function _exchanger() internal view returns (IExchanger) { return IExchanger(requireAndGetAddress(CONTRACT_EXCHANGER)); } function _systemStatus() internal view returns (ISystemStatus) { return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS)); } function _manager() internal view returns (IFuturesMarketManagerInternal) { return IFuturesMarketManagerInternal(requireAndGetAddress(CONTRACT_FUTURESMARKETMANAGER)); } function _settings() internal view returns (address) { return requireAndGetAddress(CONTRACT_FUTURESMARKETSETTINGS); } /* ---------- Market Details ---------- */ /* * The size of the skew relative to the size of the market skew scaler. * This value can be outside of [-1, 1] values. * Scaler used for skew is at skewScaleUSD to prevent extreme funding rates for small markets. */ function _proportionalSkew(uint price) internal view returns (int) { // marketSize is in baseAsset units so we need to convert from USD units require(price > 0, "price can't be zero"); uint skewScaleBaseAsset = _skewScaleUSD(marketKey).divideDecimal(price); require(skewScaleBaseAsset != 0, "skewScale is zero"); // don't divide by zero return int(marketSkew).divideDecimal(int(skewScaleBaseAsset)); } function _currentFundingRate(uint price) internal view returns (int) { int maxFundingRate = int(_maxFundingRate(marketKey)); // Note the minus sign: funding flows in the opposite direction to the skew. return _min(_max(-_UNIT, -_proportionalSkew(price)), _UNIT).multiplyDecimal(maxFundingRate); } function _unrecordedFunding(uint price) internal view returns (int funding) { int elapsed = int(block.timestamp.sub(fundingLastRecomputed)); // The current funding rate, rescaled to a percentage per second. int currentFundingRatePerSecond = _currentFundingRate(price) / 1 days; return currentFundingRatePerSecond.multiplyDecimal(int(price)).mul(elapsed); } /* * The new entry in the funding sequence, appended when funding is recomputed. It is the sum of the * last entry and the unrecorded funding, so the sequence accumulates running total over the market's lifetime. */ function _nextFundingEntry(uint price) internal view returns (int funding) { return int(fundingSequence[_latestFundingIndex()]).add(_unrecordedFunding(price)); } function _netFundingPerUnit(uint startIndex, uint price) internal view returns (int) { // Compute the net difference between start and end indices. return _nextFundingEntry(price).sub(fundingSequence[startIndex]); } /* ---------- Position Details ---------- */ /* * Determines whether a change in a position's size would violate the max market value constraint. */ function _orderSizeTooLarge( uint maxSize, int oldSize, int newSize ) internal view returns (bool) { // Allow users to reduce an order no matter the market conditions. if (_sameSide(oldSize, newSize) && _abs(newSize) <= _abs(oldSize)) { return false; } // Either the user is flipping sides, or they are increasing an order on the same side they're already on; // we check that the side of the market their order is on would not break the limit. int newSkew = int(marketSkew).sub(oldSize).add(newSize); int newMarketSize = int(marketSize).sub(_signedAbs(oldSize)).add(_signedAbs(newSize)); int newSideSize; if (0 < newSize) { // long case: marketSize + skew // = (|longSize| + |shortSize|) + (longSize + shortSize) // = 2 * longSize newSideSize = newMarketSize.add(newSkew); } else { // short case: marketSize - skew // = (|longSize| + |shortSize|) - (longSize + shortSize) // = 2 * -shortSize newSideSize = newMarketSize.sub(newSkew); } // newSideSize still includes an extra factor of 2 here, so we will divide by 2 in the actual condition if (maxSize < _abs(newSideSize.div(2))) { return true; } return false; } function _notionalValue(int positionSize, uint price) internal pure returns (int value) { return positionSize.multiplyDecimal(int(price)); } function _profitLoss(Position memory position, uint price) internal pure returns (int pnl) { int priceShift = int(price).sub(int(position.lastPrice)); return int(position.size).multiplyDecimal(priceShift); } function _accruedFunding(Position memory position, uint price) internal view returns (int funding) { uint lastModifiedIndex = position.lastFundingIndex; if (lastModifiedIndex == 0) { return 0; // The position does not exist -- no funding. } int net = _netFundingPerUnit(lastModifiedIndex, price); return int(position.size).multiplyDecimal(net); } /* * The initial margin of a position, plus any PnL and funding it has accrued. The resulting value may be negative. */ function _marginPlusProfitFunding(Position memory position, uint price) internal view returns (int) { int funding = _accruedFunding(position, price); return int(position.margin).add(_profitLoss(position, price)).add(funding); } /* * The value in a position's margin after a deposit or withdrawal, accounting for funding and profit. * If the resulting margin would be negative or below the liquidation threshold, an appropriate error is returned. * If the result is not an error, callers of this function that use it to update a position's margin * must ensure that this is accompanied by a corresponding debt correction update, as per `_applyDebtCorrection`. */ function _recomputeMarginWithDelta( Position memory position, uint price, int marginDelta ) internal view returns (uint margin, Status statusCode) { int newMargin = _marginPlusProfitFunding(position, price).add(marginDelta); if (newMargin < 0) { return (0, Status.InsufficientMargin); } uint uMargin = uint(newMargin); int positionSize = int(position.size); // minimum margin beyond which position can be liquidated uint lMargin = _liquidationMargin(positionSize, price); if (positionSize != 0 && uMargin <= lMargin) { return (uMargin, Status.CanLiquidate); } return (uMargin, Status.Ok); } function _remainingMargin(Position memory position, uint price) internal view returns (uint) { int remaining = _marginPlusProfitFunding(position, price); // If the margin went past zero, the position should have been liquidated - return zero remaining margin. return uint(_max(0, remaining)); } function _accessibleMargin(Position memory position, uint price) internal view returns (uint) { // Ugly solution to rounding safety: leave up to an extra tenth of a cent in the account/leverage // This should guarantee that the value returned here can always been withdrawn, but there may be // a little extra actually-accessible value left over, depending on the position size and margin. uint milli = uint(_UNIT / 1000); int maxLeverage = int(_maxLeverage(marketKey).sub(milli)); uint inaccessible = _abs(_notionalValue(position.size, price).divideDecimal(maxLeverage)); // If the user has a position open, we'll enforce a min initial margin requirement. if (0 < inaccessible) { uint minInitialMargin = _minInitialMargin(); if (inaccessible < minInitialMargin) { inaccessible = minInitialMargin; } inaccessible = inaccessible.add(milli); } uint remaining = _remainingMargin(position, price); if (remaining <= inaccessible) { return 0; } return remaining.sub(inaccessible); } /** * The fee charged from the margin during liquidation. Fee is proportional to position size * but is at least the _minKeeperFee() of sUSD to prevent underincentivising * liquidations of small positions. * @param positionSize size of position in fixed point decimal baseAsset units * @param price price of single baseAsset unit in sUSD fixed point decimal units * @return lFee liquidation fee to be paid to liquidator in sUSD fixed point decimal units */ function _liquidationFee(int positionSize, uint price) internal view returns (uint lFee) { // size * price * fee-ratio uint proportionalFee = _abs(positionSize).multiplyDecimal(price).multiplyDecimal(_liquidationFeeRatio()); uint minFee = _minKeeperFee(); // max(proportionalFee, minFee) - to prevent not incentivising liquidations enough return proportionalFee > minFee ? proportionalFee : minFee; // not using _max() helper because it's for signed ints } /** * The minimal margin at which liquidation can happen. Is the sum of liquidationBuffer and liquidationFee * @param positionSize size of position in fixed point decimal baseAsset units * @param price price of single baseAsset unit in sUSD fixed point decimal units * @return lMargin liquidation margin to maintain in sUSD fixed point decimal units * @dev The liquidation margin contains a buffer that is proportional to the position * size. The buffer should prevent liquidation happenning at negative margin (due to next price being worse) * so that stakers would not leak value to liquidators through minting rewards that are not from the * account's margin. */ function _liquidationMargin(int positionSize, uint price) internal view returns (uint lMargin) { uint liquidationBuffer = _abs(positionSize).multiplyDecimal(price).multiplyDecimal(_liquidationBufferRatio()); return liquidationBuffer.add(_liquidationFee(positionSize, price)); } function _canLiquidate(Position memory position, uint price) internal view returns (bool) { // No liquidating empty positions. if (position.size == 0) { return false; } return _remainingMargin(position, price) <= _liquidationMargin(int(position.size), price); } function _currentLeverage( Position memory position, uint price, uint remainingMargin_ ) internal pure returns (int leverage) { // No position is open, or it is ready to be liquidated; leverage goes to nil if (remainingMargin_ == 0) { return 0; } return _notionalValue(position.size, price).divideDecimal(int(remainingMargin_)); } function _orderFee(TradeParams memory params, uint dynamicFeeRate) internal view returns (uint fee) { // usd value of the difference in position int notionalDiff = params.sizeDelta.multiplyDecimal(int(params.price)); // If the order is submitted on the same side as the skew (increasing it) - the taker fee is charged. // Otherwise if the order is opposite to the skew, the maker fee is charged. // the case where the order flips the skew is ignored for simplicity due to being negligible // in both size of effect and frequency of occurrence uint staticRate = _sameSide(notionalDiff, marketSkew) ? params.takerFee : params.makerFee; uint feeRate = staticRate.add(dynamicFeeRate); return _abs(notionalDiff.multiplyDecimal(int(feeRate))); } /// Uses the exchanger to get the dynamic fee (SIP-184) for trading from sUSD to baseAsset /// this assumes dynamic fee is symmetric in direction of trade. /// @dev this is a pretty expensive action in terms of execution gas as it queries a lot /// of past rates from oracle. Shoudn't be much of an issue on a rollup though. function _dynamicFeeRate() internal view returns (uint feeRate, bool tooVolatile) { return _exchanger().dynamicFeeRateForExchange(sUSD, baseAsset); } function _latestFundingIndex() internal view returns (uint) { return fundingSequence.length.sub(1); // at least one element is pushed in constructor } function _postTradeDetails(Position memory oldPos, TradeParams memory params) internal view returns ( Position memory newPosition, uint fee, Status tradeStatus ) { // Reverts if the user is trying to submit a size-zero order. if (params.sizeDelta == 0) { return (oldPos, 0, Status.NilOrder); } // The order is not submitted if the user's existing position needs to be liquidated. if (_canLiquidate(oldPos, params.price)) { return (oldPos, 0, Status.CanLiquidate); } // get the dynamic fee rate SIP-184 (uint dynamicFeeRate, bool tooVolatile) = _dynamicFeeRate(); if (tooVolatile) { return (oldPos, 0, Status.PriceTooVolatile); } // calculate the total fee for exchange fee = _orderFee(params, dynamicFeeRate); // Deduct the fee. // It is an error if the realised margin minus the fee is negative or subject to liquidation. (uint newMargin, Status status) = _recomputeMarginWithDelta(oldPos, params.price, -int(fee)); if (_isError(status)) { return (oldPos, 0, status); } // construct new position Position memory newPos = Position({ id: oldPos.id, lastFundingIndex: uint64(_latestFundingIndex()), margin: uint128(newMargin), lastPrice: uint128(params.price), size: int128(int(oldPos.size).add(params.sizeDelta)) }); // always allow to decrease a position, otherwise a margin of minInitialMargin can never // decrease a position as the price goes against them. // we also add the paid out fee for the minInitialMargin because otherwise minInitialMargin // is never the actual minMargin, because the first trade will always deduct // a fee (so the margin that otherwise would need to be transferred would have to include the future // fee as well, making the UX and definition of min-margin confusing). bool positionDecreasing = _sameSide(oldPos.size, newPos.size) && _abs(newPos.size) < _abs(oldPos.size); if (!positionDecreasing) { // minMargin + fee <= margin is equivalent to minMargin <= margin - fee // except that we get a nicer error message if fee > margin, rather than arithmetic overflow. if (uint(newPos.margin).add(fee) < _minInitialMargin()) { return (oldPos, 0, Status.InsufficientMargin); } } // check that new position margin is above liquidation margin // (above, in _recomputeMarginWithDelta() we checked the old position, here we check the new one) // Liquidation margin is considered without a fee, because it wouldn't make sense to allow // a trade that will make the position liquidatable. if (newMargin <= _liquidationMargin(newPos.size, params.price)) { return (newPos, 0, Status.CanLiquidate); } // Check that the maximum leverage is not exceeded when considering new margin including the paid fee. // The paid fee is considered for the benefit of UX of allowed max leverage, otherwise, the actual // max leverage is always below the max leverage parameter since the fee paid for a trade reduces the margin. // We'll allow a little extra headroom for rounding errors. { // stack too deep int leverage = int(newPos.size).multiplyDecimal(int(params.price)).divideDecimal(int(newMargin.add(fee))); if (_maxLeverage(marketKey).add(uint(_UNIT) / 100) < _abs(leverage)) { return (oldPos, 0, Status.MaxLeverageExceeded); } } // Check that the order isn't too large for the market. // Allow a bit of extra value in case of rounding errors. if ( _orderSizeTooLarge( uint(int(_maxMarketValueUSD(marketKey).add(100 * uint(_UNIT))).divideDecimal(int(params.price))), oldPos.size, newPos.size ) ) { return (oldPos, 0, Status.MaxMarketSizeExceeded); } return (newPos, fee, Status.Ok); } /* ---------- Utilities ---------- */ /* * Absolute value of the input, returned as a signed number. */ function _signedAbs(int x) internal pure returns (int) { return x < 0 ? -x : x; } /* * Absolute value of the input, returned as an unsigned number. */ function _abs(int x) internal pure returns (uint) { return uint(_signedAbs(x)); } function _max(int x, int y) internal pure returns (int) { return x < y ? y : x; } function _min(int x, int y) internal pure returns (int) { return x < y ? x : y; } // True if and only if two positions a and b are on the same side of the market; // that is, if they have the same sign, or either of them is zero. function _sameSide(int a, int b) internal pure returns (bool) { return (a >= 0) == (b >= 0); } /* * True if and only if the given status indicates an error. */ function _isError(Status status) internal pure returns (bool) { return status != Status.Ok; } /* * Revert with an appropriate message if the first argument is true. */ function _revertIfError(bool isError, Status status) internal view { if (isError) { revert(_errorMessages[uint8(status)]); } } /* * Revert with an appropriate message if the input is an error. */ function _revertIfError(Status status) internal view { if (_isError(status)) { revert(_errorMessages[uint8(status)]); } } /* * The current base price from the oracle, and whether that price was invalid. Zero prices count as invalid. * Public because used both externally and internally */ function assetPrice() public view returns (uint price, bool invalid) { (price, invalid) = _exchangeCircuitBreaker().rateWithInvalid(baseAsset); // Ensure we catch uninitialised rates or suspended state / synth invalid = invalid || price == 0 || _systemStatus().synthSuspended(baseAsset); return (price, invalid); } /* ========== MUTATIVE FUNCTIONS ========== */ /* ---------- Market Operations ---------- */ /* * The current base price, reverting if it is invalid, or if system or synth is suspended. * This is mutative because the circuit breaker stores the last price on every invocation. */ function _assetPriceRequireSystemChecks() internal returns (uint) { // check that futures market isn't suspended, revert with appropriate message _systemStatus().requireFuturesMarketActive(marketKey); // asset and market may be different // check that synth is active, and wasn't suspended, revert with appropriate message _systemStatus().requireSynthActive(baseAsset); // check if circuit breaker if price is within deviation tolerance and system & synth is active // note: rateWithBreakCircuit (mutative) is used here instead of rateWithInvalid (view). This is // despite reverting immediately after if circuit is broken, which may seem silly. // This is in order to persist last-rate in exchangeCircuitBreaker in the happy case // because last-rate is what used for measuring the deviation for subsequent trades. (uint price, bool circuitBroken) = _exchangeCircuitBreaker().rateWithBreakCircuit(baseAsset); // revert if price is invalid or circuit was broken // note: we revert here, which means that circuit is not really broken (is not persisted), this is // because the futures methods and interface are designed for reverts, and do not support no-op // return values. _revertIfError(circuitBroken, Status.InvalidPrice); return price; } function _recomputeFunding(uint price) internal returns (uint lastIndex) { uint sequenceLengthBefore = fundingSequence.length; int funding = _nextFundingEntry(price); fundingSequence.push(int128(funding)); fundingLastRecomputed = uint32(block.timestamp); emit FundingRecomputed(funding, sequenceLengthBefore, fundingLastRecomputed); return sequenceLengthBefore; } /** * Pushes a new entry to the funding sequence at the current price and funding rate. * @dev Admin only method accessible to FuturesMarketSettings. This is admin only because: * - When system parameters change, funding should be recomputed, but system may be paused * during that time for any reason, so this method needs to work even if system is paused. * But in that case, it shouldn't be accessible to external accounts. */ function recomputeFunding() external returns (uint lastIndex) { // only FuturesMarketSettings is allowed to use this method _revertIfError(msg.sender != _settings(), Status.NotPermitted); // This method is the only mutative method that uses the view _assetPrice() // and not the mutative _assetPriceRequireSystemChecks() that reverts on system flags. // This is because this method is used by system settings when changing funding related // parameters, so needs to function even when system / market is paused. E.g. to facilitate // market migration. (uint price, bool invalid) = assetPrice(); // A check for a valid price is still in place, to ensure that a system settings action // doesn't take place when the price is invalid (e.g. some oracle issue). require(!invalid, "Invalid price"); return _recomputeFunding(price); } /* * The impact of a given position on the debt correction. */ function _positionDebtCorrection(Position memory position) internal view returns (int) { /** This method only returns the correction term for the debt calculation of the position, and not it's debt. This is needed for keeping track of the _marketDebt() in an efficient manner to allow O(1) marketDebt calculation in _marketDebt(). Explanation of the full market debt calculation from the SIP https://sips.synthetix.io/sips/sip-80/: The overall market debt is the sum of the remaining margin in all positions. The intuition is that the debt of a single position is the value withdrawn upon closing that position. single position remaining margin = initial-margin + profit-loss + accrued-funding = = initial-margin + q * (price - last-price) + q * funding-accrued-per-unit = initial-margin + q * price - q * last-price + q * (funding - initial-funding) Total debt = sum ( position remaining margins ) = sum ( initial-margin + q * price - q * last-price + q * (funding - initial-funding) ) = sum( q * price ) + sum( q * funding ) + sum( initial-margin - q * last-price - q * initial-funding ) = skew * price + skew * funding + sum( initial-margin - q * ( last-price + initial-funding ) ) = skew (price + funding) + sum( initial-margin - q * ( last-price + initial-funding ) ) The last term: sum( initial-margin - q * ( last-price + initial-funding ) ) being the position debt correction that is tracked with each position change using this method. The first term and the full debt calculation using current skew, price, and funding is calculated globally in _marketDebt(). */ return int(position.margin).sub( int(position.size).multiplyDecimal(int(position.lastPrice).add(fundingSequence[position.lastFundingIndex])) ); } function _marketDebt(uint price) internal view returns (uint) { // short circuit and also convenient during setup if (marketSkew == 0 && _entryDebtCorrection == 0) { // if these are 0, the resulting calculation is necessarily zero as well return 0; } // see comment explaining this calculation in _positionDebtCorrection() int priceWithFunding = int(price).add(_nextFundingEntry(price)); int totalDebt = int(marketSkew).multiplyDecimal(priceWithFunding).add(_entryDebtCorrection); return uint(_max(totalDebt, 0)); } /* * Alter the debt correction to account for the net result of altering a position. */ function _applyDebtCorrection(Position memory newPosition, Position memory oldPosition) internal { int newCorrection = _positionDebtCorrection(newPosition); int oldCorrection = _positionDebtCorrection(oldPosition); _entryDebtCorrection = int128(int(_entryDebtCorrection).add(newCorrection).sub(oldCorrection)); } function _transferMargin( int marginDelta, uint price, address sender ) internal { // Transfer no tokens if marginDelta is 0 uint absDelta = _abs(marginDelta); if (marginDelta > 0) { // A positive margin delta corresponds to a deposit, which will be burnt from their // sUSD balance and credited to their margin account. // Ensure we handle reclamation when burning tokens. uint postReclamationAmount = _manager().burnSUSD(sender, absDelta); if (postReclamationAmount != absDelta) { // If balance was insufficient, the actual delta will be smaller marginDelta = int(postReclamationAmount); } } else if (marginDelta < 0) { // A negative margin delta corresponds to a withdrawal, which will be minted into // their sUSD balance, and debited from their margin account. _manager().issueSUSD(sender, absDelta); } else { // Zero delta is a no-op return; } Position storage position = positions[sender]; _updatePositionMargin(position, price, marginDelta); emit MarginTransferred(sender, marginDelta); emit PositionModified(position.id, sender, position.margin, position.size, 0, price, _latestFundingIndex(), 0); } // updates the stored position margin in place (on the stored position) function _updatePositionMargin( Position storage position, uint price, int marginDelta ) internal { Position memory oldPosition = position; // Determine new margin, ensuring that the result is positive. (uint margin, Status status) = _recomputeMarginWithDelta(oldPosition, price, marginDelta); _revertIfError(status); // Update the debt correction. int positionSize = position.size; uint fundingIndex = _latestFundingIndex(); _applyDebtCorrection( Position(0, uint64(fundingIndex), uint128(margin), uint128(price), int128(positionSize)), Position(0, position.lastFundingIndex, position.margin, position.lastPrice, int128(positionSize)) ); // Update the account's position with the realised margin. position.margin = uint128(margin); // We only need to update their funding/PnL details if they actually have a position open if (positionSize != 0) { position.lastPrice = uint128(price); position.lastFundingIndex = uint64(fundingIndex); // The user can always decrease their margin if they have no position, or as long as: // * they have sufficient margin to do so // * the resulting margin would not be lower than the liquidation margin or min initial margin // * the resulting leverage is lower than the maximum leverage if (marginDelta < 0) { _revertIfError( (margin < _minInitialMargin()) || (margin <= _liquidationMargin(position.size, price)) || (_maxLeverage(marketKey) < _abs(_currentLeverage(position, price, margin))), Status.InsufficientMargin ); } } } /* * Alter the amount of margin in a position. A positive input triggers a deposit; a negative one, a * withdrawal. The margin will be burnt or issued directly into/out of the caller's sUSD wallet. * Reverts on deposit if the caller lacks a sufficient sUSD balance. * Reverts on withdrawal if the amount to be withdrawn would expose an open position to liquidation. */ function transferMargin(int marginDelta) external { uint price = _assetPriceRequireSystemChecks(); _recomputeFunding(price); _transferMargin(marginDelta, price, msg.sender); } /* * Withdraws all accessible margin in a position. This will leave some remaining margin * in the account if the caller has a position open. Equivalent to `transferMargin(-accessibleMargin(sender))`. */ function withdrawAllMargin() external { address sender = msg.sender; uint price = _assetPriceRequireSystemChecks(); _recomputeFunding(price); int marginDelta = -int(_accessibleMargin(positions[sender], price)); _transferMargin(marginDelta, price, sender); } function _trade(address sender, TradeParams memory params) internal { Position storage position = positions[sender]; Position memory oldPosition = position; // Compute the new position after performing the trade (Position memory newPosition, uint fee, Status status) = _postTradeDetails(oldPosition, params); _revertIfError(status); // Update the aggregated market size and skew with the new order size marketSkew = int128(int(marketSkew).add(newPosition.size).sub(oldPosition.size)); marketSize = uint128(uint(marketSize).add(_abs(newPosition.size)).sub(_abs(oldPosition.size))); // Send the fee to the fee pool if (0 < fee) { _manager().payFee(fee); // emit tracking code event if (params.trackingCode != bytes32(0)) { emit FuturesTracking(params.trackingCode, baseAsset, marketKey, params.sizeDelta, fee); } } // Update the margin, and apply the resulting debt correction position.margin = newPosition.margin; _applyDebtCorrection(newPosition, oldPosition); // Record the trade uint64 id = oldPosition.id; uint fundingIndex = _latestFundingIndex(); if (newPosition.size == 0) { // If the position is being closed, we no longer need to track these details. delete position.id; delete position.size; delete position.lastPrice; delete position.lastFundingIndex; } else { if (oldPosition.size == 0) { // New positions get new ids. id = _nextPositionId; _nextPositionId += 1; } position.id = id; position.size = newPosition.size; position.lastPrice = uint128(params.price); position.lastFundingIndex = uint64(fundingIndex); } // emit the modification event emit PositionModified( id, sender, newPosition.margin, newPosition.size, params.sizeDelta, params.price, fundingIndex, fee ); } /* * Adjust the sender's position size. * Reverts if the resulting position is too large, outside the max leverage, or is liquidating. */ function modifyPosition(int sizeDelta) external { _modifyPosition(sizeDelta, bytes32(0)); } /* * Same as modifyPosition, but emits an event with the passed tracking code to * allow offchain calculations for fee sharing with originating integrations */ function modifyPositionWithTracking(int sizeDelta, bytes32 trackingCode) external { _modifyPosition(sizeDelta, trackingCode); } function _modifyPosition(int sizeDelta, bytes32 trackingCode) internal { uint price = _assetPriceRequireSystemChecks(); _recomputeFunding(price); _trade( msg.sender, TradeParams({ sizeDelta: sizeDelta, price: price, takerFee: _takerFee(marketKey), makerFee: _makerFee(marketKey), trackingCode: trackingCode }) ); } /* * Submit an order to close a position. */ function closePosition() external { _closePosition(bytes32(0)); } /// Same as closePosition, but emits an even with the trackingCode for volume source fee sharing function closePositionWithTracking(bytes32 trackingCode) external { _closePosition(trackingCode); } function _closePosition(bytes32 trackingCode) internal { int size = positions[msg.sender].size; _revertIfError(size == 0, Status.NoPositionOpen); uint price = _assetPriceRequireSystemChecks(); _recomputeFunding(price); _trade( msg.sender, TradeParams({ sizeDelta: -size, price: price, takerFee: _takerFee(marketKey), makerFee: _makerFee(marketKey), trackingCode: trackingCode }) ); } function _liquidatePosition( address account, address liquidator, uint price ) internal { Position storage position = positions[account]; // get remaining margin for sending any leftover buffer to fee pool uint remMargin = _remainingMargin(position, price); // Record updates to market size and debt. int positionSize = position.size; uint positionId = position.id; marketSkew = int128(int(marketSkew).sub(positionSize)); marketSize = uint128(uint(marketSize).sub(_abs(positionSize))); uint fundingIndex = _latestFundingIndex(); _applyDebtCorrection( Position(0, uint64(fundingIndex), 0, uint128(price), 0), Position(0, position.lastFundingIndex, position.margin, position.lastPrice, int128(positionSize)) ); // Close the position itself. delete positions[account]; // Issue the reward to the liquidator. uint liqFee = _liquidationFee(positionSize, price); _manager().issueSUSD(liquidator, liqFee); emit PositionModified(positionId, account, 0, 0, 0, price, fundingIndex, 0); emit PositionLiquidated(positionId, account, liquidator, positionSize, price, liqFee); // Send any positive margin buffer to the fee pool if (remMargin > liqFee) { _manager().payFee(remMargin.sub(liqFee)); } } /* * Liquidate a position if its remaining margin is below the liquidation fee. This succeeds if and only if * `canLiquidate(account)` is true, and reverts otherwise. * Upon liquidation, the position will be closed, and the liquidation fee minted into the liquidator's account. */ function liquidatePosition(address account) external { uint price = _assetPriceRequireSystemChecks(); _recomputeFunding(price); _revertIfError(!_canLiquidate(positions[account], price), Status.CannotLiquidate); _liquidatePosition(account, msg.sender, price); } /* ========== EVENTS ========== */ event MarginTransferred(address indexed account, int marginDelta); event PositionModified( uint indexed id, address indexed account, uint margin, int size, int tradeSize, uint lastPrice, uint fundingIndex, uint fee ); event PositionLiquidated( uint indexed id, address indexed account, address indexed liquidator, int size, uint price, uint fee ); event FundingRecomputed(int funding, uint index, uint timestamp); event FuturesTracking(bytes32 indexed trackingCode, bytes32 baseAsset, bytes32 marketKey, int sizeDelta, uint fee); }