-
Notifications
You must be signed in to change notification settings - Fork 3
/
BathPair.sol
638 lines (567 loc) · 23.4 KB
/
BathPair.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
// SPDX-License-Identifier: BUSL-1.1
/// @author Rubicon DeFi Inc. - bghughes.eth
/// @notice This contract allows a strategist to use user funds in order to market make for a Rubicon pair
/// @notice The BathPair is the admin for the pair's liquidity and has many security checks in place
/// @notice This contract is also where strategists claim rewards for successful market making
pragma solidity =0.7.6;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "../interfaces/IBathToken.sol";
import "../interfaces/IBathHouse.sol";
import "../interfaces/IRubiconMarket.sol";
import "../interfaces/IStrategistUtility.sol";
contract BathPair {
/// *** Libraries ***
using SafeMath for uint256;
using SafeMath for uint16;
/// *** Storage Variables ***
/// @notice The Bath House admin of this contract; used with onlyBathHouse()
address public bathHouse;
/// @notice The Rubicon Market strategists direct all activity towards. There is only one market, RubiconMarket.sol, in the Rubicon Protocol
address public RubiconMarketAddress;
/// @notice The initialization status of BathPair
bool public initialized;
/// @dev Keeping deprecated variables maintains consistent network-agnostic contract abis when moving to new chains and versions
int128 internal deprecatedStorageVarKept420Proxy;
/// @notice Intentionally unused DEPRECATED STORAGE VARIABLE to maintain contiguous state on proxy-wrapped contracts. Consider it a beautiful scar of incremental progress 📈
/// @dev Keeping deprecated variables maintains consistent network-agnostic contract abis when moving to new chains and versions
uint256 public deprecatedStorageVarKept4Proxy;
/// @dev The id of the last StrategistTrade made by any strategist on this contract
/// @dev This value is globally unique, and increments with every trade
uint256 internal last_stratTrade_id;
/// @notice The total amount of successful offer fills that all strategists have made for a given asset
mapping(address => uint256) public totalFillsPerAsset;
/// @notice Unique id => StrategistTrade created in marketMaking call
mapping(uint256 => StrategistTrade) public strategistTrades;
/// @notice Map a strategist to their outstanding order IDs
mapping(address => mapping(address => mapping(address => uint256[])))
public outOffersByStrategist;
/// @notice Tracks the market-kaing fill amounts on a per-asset basis of a strategist
/// @dev strategist => erc20asset => fill amount per asset;
mapping(address => mapping(address => uint256)) public strategist2Fills;
/// *** Structs ***
struct order {
uint256 pay_amt;
IERC20 pay_gem;
uint256 buy_amt;
IERC20 buy_gem;
}
struct StrategistTrade {
uint256 askId;
uint256 askPayAmt;
address askAsset;
uint256 bidId;
uint256 bidPayAmt;
address bidAsset;
address strategist;
uint256 timestamp;
}
/// *** Events ***
/// @notice Log a new market-making trade placed by a strategist, resulting in a StrategitTrade
event LogStrategistTrade(
uint256 strategistTradeID,
bytes32 askId,
bytes32 bidId,
address askAsset,
address bidAsset,
uint256 timestamp,
address strategist
);
/// @notice Logs the cancellation of a StrategistTrade
event LogScrubbedStratTrade(
uint256 strategistIDScrubbed,
uint256 assetFill,
address assetAddress,
address bathAssetAddress,
uint256 quoteFill,
address quoteAddress,
address bathQuoteAddress
);
/// @notice Log when a strategist claims their market-making rewards (effectively a rebate for good performance)
event LogStrategistRewardClaim(
address strategist,
address asset,
uint256 amountOfReward,
uint256 timestamp
);
/// *** External Functions ***
/// @notice Constructor-like initialization function
/// @dev Proxy-safe initialization of storage
function initialize(uint256 _maxOrderSizeBPS, int128 _shapeCoefNum)
external
{
require(!initialized);
address _bathHouse = msg.sender; //Assume the initializer is BathHouse
require(
IBathHouse(_bathHouse).getMarket() !=
address(0x0000000000000000000000000000000000000000) &&
IBathHouse(_bathHouse).initialized(),
"BathHouse not initialized"
);
bathHouse = _bathHouse;
RubiconMarketAddress = IBathHouse(_bathHouse).getMarket();
// Shape variables for dynamic inventory management
/// *** DEprecate but keep storage variable on OP
deprecatedStorageVarKept4Proxy = _maxOrderSizeBPS;
/// @dev A deprecated storage variable! Turns out order books are elegant and complex math is simply computed off-chain, and priced in on-chain orders at the speed of Ethereum L2s!
deprecatedStorageVarKept420Proxy = _shapeCoefNum;
initialized = true;
}
/// *** Modifiers ***
modifier onlyBathHouse() {
require(msg.sender == bathHouse);
_;
}
modifier onlyApprovedStrategist(address targetStrategist) {
require(
IBathHouse(bathHouse).isApprovedStrategist(targetStrategist) ==
true,
"you are not an approved strategist - bathPair"
);
_;
}
// *** Internal Functions ***
/// @notice This function enforces that the Bath House reserveRatio (a % of underlying pool liquidity) is enforced across all pools
/// @dev This function should ensure that reserveRatio % of the underlying liquidity always remains on the Bath Token. Utilization should be 1 - reserveRatio in practice assuming strategists use all available liquidity.
function enforceReserveRatio(
address underlyingAsset,
address underlyingQuote
)
internal
view
returns (address bathAssetAddress, address bathQuoteAddress)
{
bathAssetAddress = IBathHouse(bathHouse).tokenToBathToken(
underlyingAsset
);
bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(
underlyingQuote
);
require(
(
IBathToken(bathAssetAddress).underlyingBalance().mul(
IBathHouse(bathHouse).reserveRatio()
)
).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress),
"Failed to meet asset pool reserve ratio"
);
require(
(
IBathToken(bathQuoteAddress).underlyingBalance().mul(
IBathHouse(bathHouse).reserveRatio()
)
).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress),
"Failed to meet quote pool reserve ratio"
);
}
/// @notice Log whenever a strategist rebalances a fill amount and log the amount while incrementing total fills for that specific asset
/// @dev Only log fills for each strategist in an asset specific manner
/// @dev Goal is to map a strategist to a fill
function logFill(
uint256 amt,
address strategist,
address asset
) internal {
strategist2Fills[strategist][asset] += amt;
totalFillsPerAsset[asset] += amt;
}
/// @notice Internal function to provide the next unique StrategistTrade ID
function _next_id() internal returns (uint256) {
last_stratTrade_id++;
return last_stratTrade_id;
}
/// @notice This function results in the removal of the Strategist Trade (bid and/or ask on Rubicon Market) from the books and it being deleted from the contract
/// @dev The local array of strategist IDs that exists for any given strategist [query via getOutstandingStrategistTrades()] acts as an acitve RAM for outstanding strategist trades
/// @dev Cancels outstanding orders and manages the ledger of outstandingAmount() on bathTokens as Strategist Trades are cancelled/scrubbed or expired
function handleStratOrderAtID(uint256 id) internal {
StrategistTrade memory info = strategistTrades[id];
address _asset = info.askAsset;
address _quote = info.bidAsset;
address bathAssetAddress = IBathHouse(bathHouse).tokenToBathToken(
_asset
);
address bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(
_quote
);
order memory offer1 = getOfferInfo(info.askId); //ask
order memory offer2 = getOfferInfo(info.bidId); //bid
uint256 askDelta = info.askPayAmt - offer1.pay_amt;
uint256 bidDelta = info.bidPayAmt - offer2.pay_amt;
// if real
if (info.askId != 0) {
// if delta > 0 - delta is fill => handle any amount of fill here
if (askDelta > 0) {
logFill(askDelta, info.strategist, info.askAsset);
IBathToken(bathAssetAddress).removeFilledTradeAmount(askDelta);
// not a full fill
if (askDelta != info.askPayAmt) {
IBathToken(bathAssetAddress).cancel(
info.askId,
info.askPayAmt.sub(askDelta)
);
}
}
// otherwise didn't fill so cancel
else {
IBathToken(bathAssetAddress).cancel(info.askId, info.askPayAmt); // pas amount too
}
}
// if real
if (info.bidId != 0) {
// if delta > 0 - delta is fill => handle any amount of fill here
if (bidDelta > 0) {
logFill(bidDelta, info.strategist, info.bidAsset);
IBathToken(bathQuoteAddress).removeFilledTradeAmount(bidDelta);
// not a full fill
if (bidDelta != info.bidPayAmt) {
IBathToken(bathQuoteAddress).cancel(
info.bidId,
info.bidPayAmt.sub(bidDelta)
);
}
}
// otherwise didn't fill so cancel
else {
IBathToken(bathQuoteAddress).cancel(info.bidId, info.bidPayAmt); // pass amount too
}
}
// Delete the order from outOffersByStrategist
uint256 target = getIndexFromElement(
id,
outOffersByStrategist[_asset][_quote][info.strategist]
);
uint256[] storage current = outOffersByStrategist[_asset][_quote][
info.strategist
];
current[target] = current[current.length - 1];
current.pop(); // Assign the last value to the value we want to delete and pop, best way to do this in solc AFAIK
emit LogScrubbedStratTrade(
id,
askDelta,
_asset,
bathAssetAddress,
bidDelta,
_quote,
bathQuoteAddress
);
}
/// @notice Get information about a Rubicon Market offer and return it as an order
function getOfferInfo(uint256 id) internal view returns (order memory) {
(
uint256 ask_amt,
IERC20 ask_gem,
uint256 bid_amt,
IERC20 bid_gem
) = IRubiconMarket(RubiconMarketAddress).getOffer(id);
order memory offerInfo = order(ask_amt, ask_gem, bid_amt, bid_gem);
return offerInfo;
}
/// @notice A function that returns the index of uid from array
/// @dev uid must be in array for the purposes of this contract to enforce outstanding trades per strategist are tracked correctly
function getIndexFromElement(uint256 uid, uint256[] storage array)
internal
view
returns (uint256 _index)
{
bool assigned = false;
for (uint256 index = 0; index < array.length; index++) {
if (uid == array[index]) {
_index = index;
assigned = true;
return _index;
}
}
require(assigned, "Didnt Find that element in live list, cannot scrub");
}
// *** External Functions - Only Approved Strategists ***
/// @notice Key entry point for strategists to use Bath Token (LP) funds to place market-making trades on the Rubicon Order Book
function placeMarketMakingTrades(
address[2] memory tokenPair, // ASSET, Then Quote
uint256 askNumerator, // Quote / Asset
uint256 askDenominator, // Asset / Quote
uint256 bidNumerator, // size in ASSET
uint256 bidDenominator // size in QUOTES
) public onlyApprovedStrategist(msg.sender) returns (uint256 id) {
// Require at least one order is non-zero
require(
(askNumerator > 0 && askDenominator > 0) ||
(bidNumerator > 0 && bidDenominator > 0),
"one order must be non-zero"
);
address _underlyingAsset = tokenPair[0];
address _underlyingQuote = tokenPair[1];
(
address bathAssetAddress,
address bathQuoteAddress
) = enforceReserveRatio(_underlyingAsset, _underlyingQuote);
require(
bathAssetAddress != address(0) && bathQuoteAddress != address(0),
"tokenToBathToken error"
);
// Calculate new bid and/or ask
order memory ask = order(
askNumerator,
IERC20(_underlyingAsset),
askDenominator,
IERC20(_underlyingQuote)
);
order memory bid = order(
bidNumerator,
IERC20(_underlyingQuote),
bidDenominator,
IERC20(_underlyingAsset)
);
// Place new bid and/or ask
// Note: placeOffer returns a zero if an incomplete order
uint256 newAskID = IBathToken(bathAssetAddress).placeOffer(
ask.pay_amt,
ask.pay_gem,
ask.buy_amt,
ask.buy_gem
);
uint256 newBidID = IBathToken(bathQuoteAddress).placeOffer(
bid.pay_amt,
bid.pay_gem,
bid.buy_amt,
bid.buy_gem
);
// Strategist trade is recorded so they can get paid and the trade is logged for time
StrategistTrade memory outgoing = StrategistTrade(
newAskID,
ask.pay_amt,
_underlyingAsset,
newBidID,
bid.pay_amt,
_underlyingQuote,
msg.sender,
block.timestamp
);
// Give each trade a unique id for easy handling by strategists
id = _next_id();
strategistTrades[id] = outgoing;
// Allow strategists to easily call a list of their outstanding offers
outOffersByStrategist[_underlyingAsset][_underlyingQuote][msg.sender]
.push(id);
emit LogStrategistTrade(
id,
bytes32(outgoing.askId),
bytes32(outgoing.bidId),
outgoing.askAsset,
outgoing.bidAsset,
block.timestamp,
outgoing.strategist
);
}
/// @notice A function to batch together many placeMarketMakingTrades() in a single transaction
function batchMarketMakingTrades(
address[2] memory tokenPair, // ASSET, Then Quote
uint256[] memory askNumerators, // Quote / Asset
uint256[] memory askDenominators, // Asset / Quote
uint256[] memory bidNumerators, // size in ASSET
uint256[] memory bidDenominators // size in QUOTES
) external onlyApprovedStrategist(msg.sender) {
require(
askNumerators.length == askDenominators.length &&
askDenominators.length == bidNumerators.length &&
bidNumerators.length == bidDenominators.length,
"not all order lengths match"
);
uint256 quantity = askNumerators.length;
for (uint256 index = 0; index < quantity; index++) {
placeMarketMakingTrades(
tokenPair,
askNumerators[index],
askDenominators[index],
bidNumerators[index],
bidDenominators[index]
);
}
}
/// @notice A function to requote an outstanding order and replace it with a new Strategist Trade
/// @dev Note that this function will create a new unique id for the requote'd ID due to the low-level functionality
function requote(
uint256 id,
address[2] memory tokenPair, // ASSET, Then Quote
uint256 askNumerator, // Quote / Asset
uint256 askDenominator, // Asset / Quote
uint256 bidNumerator, // size in ASSET
uint256 bidDenominator // size in QUOTES
) public onlyApprovedStrategist(msg.sender) {
// 1. Scrub strat trade
scrubStrategistTrade(id);
// 2. Place another
placeMarketMakingTrades(
tokenPair,
askNumerator,
askDenominator,
bidNumerator,
bidDenominator
);
}
/// @notice A function to batch together many requote() calls in a single transaction
/// @dev Ids and input are indexed through to execute requotes
function batchRequoteOffers(
uint256[] memory ids,
address[2] memory tokenPair, // ASSET, Then Quote
uint256[] memory askNumerators, // Quote / Asset
uint256[] memory askDenominators, // Asset / Quote
uint256[] memory bidNumerators, // size in ASSET
uint256[] memory bidDenominators // size in QUOTES
) external onlyApprovedStrategist(msg.sender) {
require(
askNumerators.length == askDenominators.length &&
askDenominators.length == bidNumerators.length &&
bidNumerators.length == bidDenominators.length &&
ids.length == askNumerators.length,
"not all input lengths match"
);
uint256 quantity = askNumerators.length;
for (uint256 index = 0; index < quantity; index++) {
requote(
ids[index],
tokenPair,
askNumerators[index],
askDenominators[index],
bidNumerators[index],
bidDenominators[index]
);
}
}
/// @notice - function to rebalance fill between two pools
function rebalancePair(
uint256 assetRebalAmt, //amount of ASSET in the quote buffer
uint256 quoteRebalAmt, //amount of QUOTE in the asset buffer
address _underlyingAsset,
address _underlyingQuote
) external onlyApprovedStrategist(msg.sender) {
address _bathHouse = bathHouse;
address _bathAssetAddress = IBathHouse(_bathHouse).tokenToBathToken(
_underlyingAsset
);
address _bathQuoteAddress = IBathHouse(_bathHouse).tokenToBathToken(
_underlyingQuote
);
require(
_bathAssetAddress != address(0) && _bathQuoteAddress != address(0),
"tokenToBathToken error"
);
// This should be localized to the bathToken in future versions
uint16 stratReward = IBathHouse(_bathHouse).getBPSToStrats();
// Simply rebalance given amounts
if (assetRebalAmt > 0) {
IBathToken(_bathQuoteAddress).rebalance(
_bathAssetAddress,
_underlyingAsset,
stratReward,
assetRebalAmt
);
}
if (quoteRebalAmt > 0) {
IBathToken(_bathAssetAddress).rebalance(
_bathQuoteAddress,
_underlyingQuote,
stratReward,
quoteRebalAmt
);
}
}
/// @notice Function to attempt inventory risk tail off on an AMM
/// @dev This function calls the strategist utility which handles the trade and returns funds to LPs
function tailOff(
address targetPool,
address tokenToHandle,
address targetToken,
address _stratUtil, // delegatecall target
uint256 amount, //fill amount to handle
uint256 hurdle, //must clear this on tail off
uint24 _poolFee
) external onlyApprovedStrategist(msg.sender) {
// transfer here
uint16 stratRewardBPS = IBathHouse(bathHouse).getBPSToStrats();
IBathToken(targetPool).rebalance(
_stratUtil,
tokenToHandle,
stratRewardBPS,
amount
);
// Should always exceed hurdle given amountOutMinimum
IStrategistUtility(_stratUtil).UNIdump(
amount.sub((stratRewardBPS.mul(amount)).div(10000)),
tokenToHandle,
targetToken,
hurdle,
_poolFee,
targetPool
);
}
/// @notice Cancel an outstanding strategist offers and return funds to LPs while logging fills
function scrubStrategistTrade(uint256 id)
public
onlyApprovedStrategist(msg.sender)
{
require(
msg.sender == strategistTrades[id].strategist,
"you are not the strategist that made this order"
);
handleStratOrderAtID(id);
}
/// @notice Batch scrub outstanding strategist trades and return funds to LPs
function scrubStrategistTrades(uint256[] memory ids)
external
onlyApprovedStrategist(msg.sender)
{
for (uint256 index = 0; index < ids.length; index++) {
uint256 _id = ids[index];
scrubStrategistTrade(_id);
}
}
/// @notice Function where strategists claim rewards proportional to their quantity of fills
/// @dev This function should allow a strategist to claim ERC20s sitting on this contract (earned via rebalancing) relative to their share or strategist activity on the pair
/// @dev Provide the pair on which you want to claim rewards
function strategistBootyClaim(address asset, address quote)
external
onlyApprovedStrategist(msg.sender)
{
uint256 fillCountA = strategist2Fills[msg.sender][asset];
uint256 fillCountQ = strategist2Fills[msg.sender][quote];
if (fillCountA > 0) {
uint256 booty = (
fillCountA.mul(IERC20(asset).balanceOf(address(this)))
).div(totalFillsPerAsset[asset]);
IERC20(asset).transfer(msg.sender, booty);
emit LogStrategistRewardClaim(
msg.sender,
asset,
booty,
block.timestamp
);
totalFillsPerAsset[asset] -= fillCountA;
strategist2Fills[msg.sender][asset] -= fillCountA;
}
if (fillCountQ > 0) {
uint256 booty = (
fillCountQ.mul(IERC20(quote).balanceOf(address(this)))
).div(totalFillsPerAsset[quote]);
IERC20(quote).transfer(msg.sender, booty);
emit LogStrategistRewardClaim(
msg.sender,
quote,
booty,
block.timestamp
);
totalFillsPerAsset[quote] -= fillCountQ;
strategist2Fills[msg.sender][quote] -= fillCountQ;
}
}
/// *** View Functions ***
/// @notice The goal of this function is to enable a means to retrieve all outstanding orders a strategist has live in the books
/// @dev This is helpful to manage orders as well as track all strategist orders (like their RAM of StratTrade IDs) and place any would-be constraints on strategists
function getOutstandingStrategistTrades(
address asset,
address quote,
address strategist
) public view returns (uint256[] memory) {
return outOffersByStrategist[asset][quote][strategist];
}
}