-
Notifications
You must be signed in to change notification settings - Fork 1
/
ProfitDistributor.sol
356 lines (304 loc) · 12.1 KB
/
ProfitDistributor.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "openzeppelin/token/ERC20/ERC20.sol";
import "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/IRewardThrottle.sol";
import "../interfaces/ILiquidityExtension.sol";
import "../interfaces/IMaltDataLab.sol";
import "../interfaces/IAuction.sol";
import "../interfaces/IBurnMintableERC20.sol";
import "../interfaces/IGlobalImpliedCollateralService.sol";
import "../interfaces/IImpliedCollateralService.sol";
import "../StabilizedPoolExtensions/StabilizedPoolUnit.sol";
import "../StabilizedPoolExtensions/LiquidityExtensionExtension.sol";
import "../StabilizedPoolExtensions/RewardThrottleExtension.sol";
import "../StabilizedPoolExtensions/SwingTraderExtension.sol";
import "../StabilizedPoolExtensions/ImpliedCollateralServiceExtension.sol";
import "../StabilizedPoolExtensions/GlobalICExtension.sol";
import "../StabilizedPoolExtensions/DataLabExtension.sol";
import "../StabilizedPoolExtensions/AuctionExtension.sol";
/// @title Profit Distributor
/// @author 0xScotch <scotch@malt.money>
/// @notice Any profit generated by the pool is pushed here and then sent to where it needs to go
contract ProfitDistributor is
StabilizedPoolUnit,
LiquidityExtensionExtension,
RewardThrottleExtension,
SwingTraderExtension,
ImpliedCollateralServiceExtension,
GlobalICExtension,
DataLabExtension,
AuctionExtension
{
using SafeERC20 for ERC20;
address public dao;
address payable public treasury;
uint256 public daoRewardCutBps;
uint256 public distributeBps = 9200; // 92%
uint256 public swingTraderPreferenceBps = 8000; // 80% to ST when pool is underperforming
uint256 public lpThrottleBps = 2500; // 25%
uint256 public maxLEContributionBps = 7000;
event SetDaoCut(uint256 daoCut);
event SetDao(address dao);
event RewardDistribution(
uint256 swingTraderCut,
uint256 lpCut,
uint256 daoCut,
uint256 treasuryCut
);
event SetTreasury(address newTreasury);
event SetMaxContribution(uint256 maxContribution);
event SetLpThrottleBps(uint256 bps);
event SetSwingTraderPreferenceBps(uint256 bps);
event SetDistributeBps(uint256 bps);
constructor(
address timelock,
address repository,
address poolFactory,
address _dao,
address payable _treasury
) StabilizedPoolUnit(timelock, repository, poolFactory) {
require(_treasury != address(0), "StabilizerNode: Treasury addr(0)");
require(_dao != address(0), "StabilizerNode: DAO addr(0)");
treasury = _treasury;
dao = _dao;
}
/// @notice Admin only method that can only be called once and will set up all the initial contract properties
/// @param _malt The address of an instance of Malt token
/// @param _collateralToken The address of an instance of Collateral Token ERC20
/// @param _globalIC The address of an instance of GlobalImpliedCollateralService
/// @param _rewardThrottle The address of an instance of RewardThrottle
/// @param _swingTrader The address of an instance of SwingTrader
/// @param _liquidityExtension The address of an instance of LiquidityExtension
/// @param _auction The address of an instance of Auction
/// @param _maltDataLab The address of an instance of MaltDataLab
/// @param _impliedCollateralService The address of an instance of ImpliedCollateralService
function setupContracts(
address _malt,
address _collateralToken,
address _globalIC,
address _rewardThrottle,
address _swingTrader,
address _liquidityExtension,
address _auction,
address _maltDataLab,
address _impliedCollateralService,
address pool
) external onlyRoleMalt(POOL_FACTORY_ROLE, "Must have pool factory role") {
require(!contractActive, "StabilizerNode: Already setup");
require(_malt != address(0), "ProfitDist: Malt addr(0)");
require(_collateralToken != address(0), "ProfitDist: Reward addr(0)");
require(_globalIC != address(0), "ProfitDist: GlobalIC addr(0)");
require(_rewardThrottle != address(0), "StabilizerNode: Throttle addr(0)");
require(_swingTrader != address(0), "StabilizerNode: Swing addr(0)");
require(_liquidityExtension != address(0), "StabilizerNode: LE addr(0)");
require(_auction != address(0), "StabilizerNode: Auction addr(0)");
require(_maltDataLab != address(0), "StabilizerNode: MaltDataLab addr(0)");
require(
_impliedCollateralService != address(0),
"StabilizerNode: ImpColSvc addr(0)"
);
contractActive = true;
collateralToken = ERC20(_collateralToken);
malt = IBurnMintableERC20(_malt);
globalIC = IGlobalImpliedCollateralService(_globalIC);
rewardThrottle = IRewardThrottle(_rewardThrottle);
swingTrader = ISwingTrader(_swingTrader);
liquidityExtension = ILiquidityExtension(_liquidityExtension);
auction = IAuction(_auction);
maltDataLab = IMaltDataLab(_maltDataLab);
impliedCollateralService = IImpliedCollateralService(
_impliedCollateralService
);
(, address updater, ) = poolFactory.getPool(pool);
_setPoolUpdater(updater);
}
/// @notice Handles distributing collateralToken profit to LPs etc
/// @param profit The amount of profit to be distributed
/// @dev It is assumed that the profit is sent here first before handleProfit is called. The balance is verified before proceeding
function handleProfit(uint256 profit) external onlyActive {
uint256 balance = collateralToken.balanceOf(address(this));
require(profit <= balance, "ProfitDist: Insufficient balance");
_distributeProfit(profit);
}
function _distributeProfit(uint256 rewarded) internal {
if (rewarded == 0) {
return;
}
rewarded = _replenishLiquidityExtension(rewarded);
if (rewarded == 0) {
return;
}
// Ensure starting at 0
collateralToken.safeApprove(address(auction), 0);
collateralToken.safeApprove(address(auction), rewarded);
rewarded = auction.allocateArbRewards(rewarded);
// Reset approval
collateralToken.safeApprove(address(auction), 0);
if (rewarded == 0) {
return;
}
uint256 distributeCut = (rewarded * distributeBps) / 10000;
uint256 daoCut = (distributeCut * daoRewardCutBps) / 10000;
distributeCut -= daoCut;
// globaIC value comes back in malt.decimals(). Convert to collateralToken.decimals
uint256 globalSwingTraderDeficit = (maltDataLab.maltToRewardDecimals(
globalIC.swingTraderCollateralDeficit()
) * maltDataLab.priceTarget()) / (10**collateralToken.decimals());
// this is already in collateralToken.decimals()
uint256 lpCut;
uint256 swingTraderCut;
if (globalSwingTraderDeficit == 0) {
lpCut = distributeCut;
} else {
uint256 runwayDeficit = rewardThrottle.runwayDeficit();
if (runwayDeficit == 0) {
swingTraderCut = distributeCut;
} else {
uint256 totalDeficit = runwayDeficit + globalSwingTraderDeficit;
uint256 globalSwingTraderRatio = maltDataLab.maltToRewardDecimals(
globalIC.swingTraderCollateralRatio()
);
// Already in collateralToken.decimals
uint256 poolSwingTraderRatio = impliedCollateralService
.swingTraderCollateralRatio();
if (poolSwingTraderRatio < globalSwingTraderRatio) {
swingTraderCut = (distributeCut * swingTraderPreferenceBps) / 10000;
lpCut = distributeCut - swingTraderCut;
} else {
lpCut =
(((distributeCut * runwayDeficit) / totalDeficit) *
(10000 - lpThrottleBps)) /
10000;
swingTraderCut = distributeCut - lpCut;
}
}
}
// Treasury gets paid after everyone else
uint256 treasuryCut = rewarded - daoCut - lpCut - swingTraderCut;
assert(treasuryCut <= rewarded);
if (swingTraderCut > 0) {
collateralToken.safeTransfer(address(swingTrader), swingTraderCut);
}
if (treasuryCut > 0) {
collateralToken.safeTransfer(treasury, treasuryCut);
}
if (daoCut > 0) {
collateralToken.safeTransfer(dao, daoCut);
}
if (lpCut > 0) {
collateralToken.safeTransfer(address(rewardThrottle), lpCut);
rewardThrottle.handleReward();
}
emit RewardDistribution(swingTraderCut, lpCut, daoCut, treasuryCut);
}
function _replenishLiquidityExtension(uint256 rewards)
internal
returns (uint256 remaining)
{
if (rewards == 0) {
return rewards;
}
(uint256 deficit, ) = liquidityExtension.collateralDeficit();
if (deficit == 0) {
return rewards;
}
uint256 maxContrib = (rewards * maxLEContributionBps) / 10000;
if (deficit >= maxContrib) {
collateralToken.safeTransfer(address(liquidityExtension), maxContrib);
return rewards - maxContrib;
}
collateralToken.safeTransfer(address(liquidityExtension), deficit);
return rewards - deficit;
}
/// @notice Admin only method for setting the cut of profit that goes to the DAO
/// @param _daoCut The % of profit to be sent to the DAO. Denominated in basis points ie 100 = 1%
function setDaoCut(uint256 _daoCut)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin role")
{
require(_daoCut <= 10000, "Reward cut must be <= 100%");
daoRewardCutBps = _daoCut;
emit SetDaoCut(_daoCut);
}
/// @notice Admin only method for setting the preference for sending to swing trader when local pool collateral ratio is less than global ratio
/// @param _pref The % of profit to be sent to SwingTrader. Represented in basis points ie 100 = 1%
function setSwingTraderPreferenceBps(uint256 _pref)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin role")
{
require(_pref <= 10000, "Must be <= 100%");
swingTraderPreferenceBps = _pref;
emit SetSwingTraderPreferenceBps(_pref);
}
/// @notice Admin only method for setting the amount LP share of profit should be throttled
/// @dev The lp throttle is used to skew system in favor of replenishing SwingTrader when all else is equal
/// @param _bps The % of LP profit that should be throttled represented in basis points ie 100 = 1%
function setLpThrottleBps(uint256 _bps)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin role")
{
require(_bps <= 10000, "Must be <= 100%");
lpThrottleBps = _bps;
emit SetLpThrottleBps(_bps);
}
/// @notice Admin only method for setting the amount of profit that should be distributed vs retained for protocol development
/// @param _bps The % of profit that should be distributed represented in basis points ie 100 = 1%
function setDistributeBps(uint256 _bps)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin role")
{
require(_bps <= 10000, "Must be <= 100%");
distributeBps = _bps;
emit SetDistributeBps(_bps);
}
/// @notice Admin only method for setting max % of profit that can be used to replenish LiquidityExtension
/// @param _maxContribution The % of profit that can be used represented in basis points ie 100 = 1%
function setMaxLEContribution(uint256 _maxContribution)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin role")
{
require(
_maxContribution != 0 && _maxContribution <= 10000,
"Must be between 0 and 100"
);
maxLEContributionBps = _maxContribution;
emit SetMaxContribution(_maxContribution);
}
/*
* Contract Pointers
*/
/// @notice Admin only method for setting the address of the Malt DAO
/// @param _dao The contract address of the Malt DAO
function setDAO(address _dao)
external
onlyRoleMalt(POOL_UPDATER_ROLE, "Must have pool updater role")
{
require(_dao != address(0), "Not address 0");
dao = _dao;
emit SetDao(_dao);
}
/// @notice Admin only method for setting the address of the treasury
/// @param _newTreasury The address of the treasury multisig
function setTreasury(address payable _newTreasury)
external
onlyRoleMalt(POOL_UPDATER_ROLE, "Must have pool updater role")
{
treasury = _newTreasury;
emit SetTreasury(_newTreasury);
}
function _accessControl()
internal
override(
LiquidityExtensionExtension,
RewardThrottleExtension,
SwingTraderExtension,
ImpliedCollateralServiceExtension,
GlobalICExtension,
DataLabExtension,
AuctionExtension
)
{
_onlyRoleMalt(POOL_UPDATER_ROLE, "Must have pool updater role");
}
}