Skip to content

Commit 73c69f4

Browse files
authored
Futures dynamic fee (#1673)
1 parent addfca8 commit 73c69f4

8 files changed

+296
-34
lines changed

contracts/ExchangeCircuitBreaker.sol

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,6 @@ contract ExchangeCircuitBreaker is Owned, MixinSystemSettings, IExchangeCircuitB
201201
return false;
202202
}
203203

204-
// ========== MODIFIERS ==========
205-
206-
modifier onlyExchangeRates() {
207-
require(msg.sender == address(_exchangeRates()), "Restricted to ExchangeRates");
208-
_;
209-
}
210-
211204
// ========== EVENTS ==========
212205

213206
// @notice signals that a the "last rate" was overriden by one of the admin methods

contracts/FuturesMarketBase.sol

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import "./SafeDecimalMath.sol";
1515
// Internal references
1616
import "./interfaces/IExchangeCircuitBreaker.sol";
1717
import "./interfaces/IExchangeRates.sol";
18+
import "./interfaces/IExchanger.sol";
1819
import "./interfaces/ISystemStatus.sol";
1920
import "./interfaces/IERC20.sol";
2021

@@ -92,6 +93,9 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
9293
// This is the same unit as used inside `SignedSafeDecimalMath`.
9394
int private constant _UNIT = int(10**uint(18));
9495

96+
//slither-disable-next-line naming-convention
97+
bytes32 internal constant sUSD = "sUSD";
98+
9599
/* ========== STATE VARIABLES ========== */
96100

97101
// The asset being traded in this market. This should be a valid key into the ExchangeRates contract.
@@ -140,6 +144,7 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
140144
/* ---------- Address Resolver Configuration ---------- */
141145

142146
bytes32 internal constant CONTRACT_CIRCUIT_BREAKER = "ExchangeCircuitBreaker";
147+
bytes32 internal constant CONTRACT_EXCHANGER = "Exchanger";
143148
bytes32 internal constant CONTRACT_FUTURESMARKETMANAGER = "FuturesMarketManager";
144149
bytes32 internal constant CONTRACT_FUTURESMARKETSETTINGS = "FuturesMarketSettings";
145150
bytes32 internal constant CONTRACT_SYSTEMSTATUS = "SystemStatus";
@@ -177,6 +182,7 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
177182
_errorMessages[uint8(Status.NotPermitted)] = "Not permitted by this address";
178183
_errorMessages[uint8(Status.NilOrder)] = "Cannot submit empty order";
179184
_errorMessages[uint8(Status.NoPositionOpen)] = "No position open";
185+
_errorMessages[uint8(Status.PriceTooVolatile)] = "Price too volatile";
180186
}
181187

182188
/* ========== VIEWS ========== */
@@ -185,18 +191,23 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
185191

186192
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
187193
bytes32[] memory existingAddresses = MixinFuturesMarketSettings.resolverAddressesRequired();
188-
bytes32[] memory newAddresses = new bytes32[](4);
189-
newAddresses[0] = CONTRACT_CIRCUIT_BREAKER;
190-
newAddresses[1] = CONTRACT_FUTURESMARKETMANAGER;
191-
newAddresses[2] = CONTRACT_FUTURESMARKETSETTINGS;
192-
newAddresses[3] = CONTRACT_SYSTEMSTATUS;
194+
bytes32[] memory newAddresses = new bytes32[](5);
195+
newAddresses[0] = CONTRACT_EXCHANGER;
196+
newAddresses[1] = CONTRACT_CIRCUIT_BREAKER;
197+
newAddresses[2] = CONTRACT_FUTURESMARKETMANAGER;
198+
newAddresses[3] = CONTRACT_FUTURESMARKETSETTINGS;
199+
newAddresses[4] = CONTRACT_SYSTEMSTATUS;
193200
addresses = combineArrays(existingAddresses, newAddresses);
194201
}
195202

196203
function _exchangeCircuitBreaker() internal view returns (IExchangeCircuitBreaker) {
197204
return IExchangeCircuitBreaker(requireAndGetAddress(CONTRACT_CIRCUIT_BREAKER));
198205
}
199206

207+
function _exchanger() internal view returns (IExchanger) {
208+
return IExchanger(requireAndGetAddress(CONTRACT_EXCHANGER));
209+
}
210+
200211
function _systemStatus() internal view returns (ISystemStatus) {
201212
return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS));
202213
}
@@ -542,22 +553,25 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
542553
return _notionalValue(position.size, price).divideDecimal(int(remainingMargin_));
543554
}
544555

545-
function _orderFee(TradeParams memory params) internal view returns (uint fee) {
556+
function _orderFee(TradeParams memory params, uint dynamicFeeRate) internal view returns (uint fee) {
557+
// usd value of the difference in position
546558
int notionalDiff = params.sizeDelta.multiplyDecimal(int(params.price));
547559

548-
if (_sameSide(notionalDiff, marketSkew)) {
549-
// If the order is submitted on the same side as the skew, increasing it.
550-
// The taker fee is charged on the increase.
551-
fee = _abs(notionalDiff.multiplyDecimal(int(params.takerFee)));
552-
} else {
553-
// Otherwise if the order is opposite to the skew,
554-
// the maker fee is charged on new notional value up to the size of the existing skew,
555-
// and the taker fee is charged on any new skew they induce on the order's side of the market.
556-
fee = _abs(notionalDiff.multiplyDecimal(int(params.makerFee)));
557-
}
558-
560+
// If the order is submitted on the same side as the skew, increasing it. The taker fee is charged.
561+
// Otherwise if the order is opposite to the skew, the maker fee is charged.
559562
// the case where the order flips the skew is ignored for simplicity due to being negligible
560563
// in both size of effect and frequency of occurrence
564+
uint staticRate = _sameSide(notionalDiff, marketSkew) ? params.takerFee : params.makerFee;
565+
uint feeRate = staticRate.add(dynamicFeeRate);
566+
return _abs(notionalDiff.multiplyDecimal(int(feeRate)));
567+
}
568+
569+
/// Uses the exchanger to get the dynamic fee (SIP-184) for trading from sUSD to baseAsset
570+
/// this assumes dynamic fee is symmetric in direction of trade.
571+
/// @dev this is a pretty expensive action in terms of execution gas as it queries a lot
572+
/// of past rates from oracle. Shoudn't be much of an issue on a rollup though.
573+
function _dynamicFeeRate() internal view returns (uint feeRate, bool tooVolatile) {
574+
return _exchanger().dynamicFeeRateForExchange(sUSD, baseAsset);
561575
}
562576

563577
function _postTradeDetails(Position memory oldPos, TradeParams memory params)
@@ -581,9 +595,17 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
581595

582596
int newSize = int(oldPos.size).add(params.sizeDelta);
583597

598+
// get the dynamic fee rate SIP-184
599+
(uint dynamicFeeRate, bool tooVolatile) = _dynamicFeeRate();
600+
if (tooVolatile) {
601+
return (oldPos, 0, Status.PriceTooVolatile);
602+
}
603+
604+
// calculate the total fee for exchange
605+
fee = _orderFee(params, dynamicFeeRate);
606+
584607
// Deduct the fee.
585608
// It is an error if the realised margin minus the fee is negative or subject to liquidation.
586-
fee = _orderFee(params);
587609
(uint newMargin, Status status) = _realisedMargin(oldPos, params.fundingIndex, params.price, -int(fee));
588610
if (_isError(status)) {
589611
return (oldPos, 0, status);
@@ -702,8 +724,15 @@ contract FuturesMarketBase is Owned, Proxyable, MixinFuturesMarketSettings, IFut
702724
// check that synth is active, and wasn't suspended, revert with appropriate message
703725
_systemStatus().requireSynthActive(baseAsset);
704726
// check if circuit breaker if price is within deviation tolerance and system & synth is active
727+
// note: rateWithBreakCircuit (mutative) is used here instead of rateWithInvalid (view). This is
728+
// despite reverting immediately after if circuit is broken, which may seem silly.
729+
// This is in order to persist last-rate in exchangeCircuitBreaker in the happy case
730+
// because last-rate is what used for measuring the deviation for subsequent trades.
705731
(uint price, bool circuitBroken) = _exchangeCircuitBreaker().rateWithBreakCircuit(baseAsset);
706732
// revert if price is invalid or circuit was broken
733+
// note: we revert here, which means that circuit is not really broken (is not persisted), this is
734+
// because the futures methods and interface are designed for reverts, and do not support no-op
735+
// return values.
707736
_revertIfError(circuitBroken, Status.InvalidPrice);
708737
return price;
709738
}

contracts/MixinFuturesNextPriceOrders.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ contract MixinFuturesNextPriceOrders is FuturesMarketBase {
125125
/**
126126
* @notice Tries to execute a previously submitted next-price order.
127127
* Reverts if:
128-
* - There is no otder
128+
* - There is no order
129129
* - Target roundId wasn't reached yet
130130
* - Order is stale (target roundId is too low compared to current roundId).
131131
* - Order fails for accounting reason (e.g. margin was removed, leverage exceeded, etc)
@@ -210,9 +210,12 @@ contract MixinFuturesNextPriceOrders is FuturesMarketBase {
210210
// modify params to spot fee
211211
params.takerFee = _takerFee(baseAsset);
212212
params.makerFee = _makerFee(baseAsset);
213-
// commit fee is equal to the spot fee that would be paid
214-
// this is to prevent free cancellation manipulations (by e.g. withdrawing the margin)
215-
return _orderFee(params);
213+
// Commit fee is equal to the spot fee that would be paid.
214+
// This is to prevent free cancellation manipulations (by e.g. withdrawing the margin).
215+
// The dynamic fee rate is passed as 0 since for the purposes of the commitment deposit
216+
// it is not important since at the time of order execution it will be refunded and the correct
217+
// dynamic fee will be charged.
218+
return _orderFee(params, 0);
216219
}
217220

218221
///// Events

contracts/MixinFuturesViews.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,15 @@ contract MixinFuturesViews is FuturesMarketBase {
225225

226226
/*
227227
* Reports the fee for submitting an order of a given size. Orders that increase the skew will be more
228-
* expensive than ones that decrease it; closing positions implies a different fee rate.
228+
* expensive than ones that decrease it. Dynamic fee is added according to the recent volatility
229+
* according to SIP-184.
230+
* @param sizeDelta size of the order in baseAsset units (negative numbers for shorts / selling)
231+
* @return fee in sUSD decimal, and invalid boolean flag for invalid rates or dynamic fee that is
232+
* too high due to recent volatility.
229233
*/
230234
function orderFee(int sizeDelta) external view returns (uint fee, bool invalid) {
231235
(uint price, bool isInvalid) = _assetPrice();
236+
(uint dynamicFeeRate, bool tooVolatile) = _dynamicFeeRate();
232237
TradeParams memory params =
233238
TradeParams({
234239
sizeDelta: sizeDelta,
@@ -237,7 +242,7 @@ contract MixinFuturesViews is FuturesMarketBase {
237242
takerFee: _takerFee(baseAsset),
238243
makerFee: _makerFee(baseAsset)
239244
});
240-
return (_orderFee(params), isInvalid);
245+
return (_orderFee(params, dynamicFeeRate), isInvalid || tooVolatile);
241246
}
242247

243248
/*

contracts/interfaces/IFuturesMarketBaseTypes.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ interface IFuturesMarketBaseTypes {
1414
InsufficientMargin,
1515
NotPermitted,
1616
NilOrder,
17-
NoPositionOpen
17+
NoPositionOpen,
18+
PriceTooVolatile
1819
}
1920

2021
// If margin/size are positive, the position is long; if negative then it is short.

0 commit comments

Comments
 (0)