forked from reflexer-labs/geb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
AccountingEngine.sol
427 lines (394 loc) · 21.7 KB
/
AccountingEngine.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
/// AccountingEngine.sol
// Copyright (C) 2018 Rain <rainbreak@riseup.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity 0.6.7;
abstract contract DebtAuctionHouseLike {
function startAuction(address incomeReceiver, uint256 amountToSell, uint256 initialBid) virtual public returns (uint256);
function protocolToken() virtual public view returns (address);
function disableContract() virtual external;
function contractEnabled() virtual public view returns (uint256);
}
abstract contract SurplusAuctionHouseLike {
function startAuction(uint256, uint256) virtual public returns (uint256);
function protocolToken() virtual public view returns (address);
function disableContract() virtual external;
function contractEnabled() virtual public view returns (uint256);
}
abstract contract SAFEEngineLike {
function coinBalance(address) virtual public view returns (uint256);
function debtBalance(address) virtual public view returns (uint256);
function settleDebt(uint256) virtual external;
function transferInternalCoins(address,address,uint256) virtual external;
function approveSAFEModification(address) virtual external;
function denySAFEModification(address) virtual external;
}
abstract contract SystemStakingPoolLike {
function canPrintProtocolTokens() virtual public view returns (bool);
}
abstract contract ProtocolTokenAuthorityLike {
function authorizedAccounts(address) virtual public view returns (uint256);
}
contract AccountingEngine {
// --- Auth ---
mapping (address => uint256) public authorizedAccounts;
/**
* @notice Add auth to an account
* @param account Account to add auth to
*/
function addAuthorization(address account) external isAuthorized {
require(contractEnabled == 1, "AccountingEngine/contract-not-enabled");
authorizedAccounts[account] = 1;
emit AddAuthorization(account);
}
/**
* @notice Remove auth from an account
* @param account Account to remove auth from
*/
function removeAuthorization(address account) external isAuthorized {
authorizedAccounts[account] = 0;
emit RemoveAuthorization(account);
}
/**
* @notice Checks whether msg.sender can call an authed function
**/
modifier isAuthorized {
require(authorizedAccounts[msg.sender] == 1, "AccountingEngine/account-not-authorized");
_;
}
// --- Data ---
// SAFE database
SAFEEngineLike public safeEngine;
// Contract that handles auctions for surplus stability fees (sell coins for protocol tokens that are then burned)
SurplusAuctionHouseLike public surplusAuctionHouse;
/**
Contract that handles auctions for debt that couldn't be covered by collateral
auctions (it prints protocol tokens in exchange for coins that will settle the debt)
**/
DebtAuctionHouseLike public debtAuctionHouse;
// Permissions registry for who can burn and mint protocol tokens
ProtocolTokenAuthorityLike public protocolTokenAuthority;
// Staking pool for protocol tokens
SystemStakingPoolLike public systemStakingPool;
// Contract that auctions extra surplus after settlement is triggered
address public postSettlementSurplusDrain;
// Address that receives extra surplus transfers
address public extraSurplusReceiver;
/**
Debt blocks that need to be covered by auctions. There is a delay to pop debt from
this queue and either settle it with surplus that came from collateral auctions or with debt auctions
that print protocol tokens
**/
mapping (uint256 => uint256) public debtQueue; // [unix timestamp => rad]
// Addresses that popped debt out of the queue
mapping (uint256 => address) public debtPoppers; // [unix timestamp => address]
// Total debt in the queue (that the system tries to cover with collateral auctions)
uint256 public totalQueuedDebt; // [rad]
// Total debt being auctioned in DebtAuctionHouse (printing protocol tokens for coins that will settle the debt)
uint256 public totalOnAuctionDebt; // [rad]
// When the last surplus auction was triggered
uint256 public lastSurplusAuctionTime; // [unix timestamp]
// When the last surplus transfer was triggered
uint256 public lastSurplusTransferTime; // [unix timestamp]
// Delay between surplus auctions
uint256 public surplusAuctionDelay; // [seconds]
// Delay between extra surplus transfers
uint256 public surplusTransferDelay; // [seconds]
// Delay after which debt can be popped from debtQueue
uint256 public popDebtDelay; // [seconds]
// Amount of protocol tokens to be minted post-auction
uint256 public initialDebtAuctionMintedTokens; // [wad]
// Amount of debt sold in one debt auction (initial coin bid for initialDebtAuctionMintedTokens protocol tokens)
uint256 public debtAuctionBidSize; // [rad]
// Whether the system transfers surplus instead of auctioning it
uint256 public extraSurplusIsTransferred;
// Amount of surplus stability fees sold in one surplus auction
uint256 public surplusAuctionAmountToSell; // [rad]
// Amount of extra surplus to transfer
uint256 public surplusTransferAmount; // [rad]
// Amount of stability fees that need to accrue in this contract before any surplus auction can start
uint256 public surplusBuffer; // [rad]
// Time to wait (post settlement) until any remaining surplus can be transferred to the settlement auctioneer
uint256 public disableCooldown; // [seconds]
// When the contract was disabled
uint256 public disableTimestamp; // [unix timestamp]
// Whether this contract is enabled or not
uint256 public contractEnabled;
// --- Events ---
event AddAuthorization(address account);
event RemoveAuthorization(address account);
event ModifyParameters(bytes32 indexed parameter, uint256 data);
event ModifyParameters(bytes32 indexed parameter, address data);
event PushDebtToQueue(uint256 indexed timestamp, uint256 debtQueueBlock, uint256 totalQueuedDebt);
event PopDebtFromQueue(uint256 indexed timestamp, uint256 debtQueueBlock, uint256 totalQueuedDebt);
event SettleDebt(uint256 rad, uint256 coinBalance, uint256 debtBalance);
event CancelAuctionedDebtWithSurplus(uint rad, uint256 totalOnAuctionDebt, uint256 coinBalance, uint256 debtBalance);
event AuctionDebt(uint256 indexed id, uint256 totalOnAuctionDebt, uint256 debtBalance);
event AuctionSurplus(uint256 indexed id, uint256 lastSurplusAuctionTime, uint256 coinBalance);
event DisableContract(uint256 disableTimestamp, uint256 disableCooldown, uint256 coinBalance, uint256 debtBalance);
event TransferPostSettlementSurplus(address postSettlementSurplusDrain, uint256 coinBalance, uint256 debtBalance);
event TransferExtraSurplus(address indexed extraSurplusReceiver, uint256 lastSurplusAuctionTime, uint256 coinBalance);
// --- Init ---
constructor(
address safeEngine_,
address surplusAuctionHouse_,
address debtAuctionHouse_
) public {
authorizedAccounts[msg.sender] = 1;
safeEngine = SAFEEngineLike(safeEngine_);
surplusAuctionHouse = SurplusAuctionHouseLike(surplusAuctionHouse_);
debtAuctionHouse = DebtAuctionHouseLike(debtAuctionHouse_);
safeEngine.approveSAFEModification(surplusAuctionHouse_);
lastSurplusAuctionTime = now;
lastSurplusTransferTime = now;
contractEnabled = 1;
emit AddAuthorization(msg.sender);
}
// --- Math ---
function addition(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x, "AccountingEngine/add-overflow");
}
function subtract(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x - y) <= x, "AccountingEngine/sub-underflow");
}
function minimum(uint256 x, uint256 y) internal pure returns (uint256 z) {
return x <= y ? x : y;
}
// --- Administration ---
/**
* @notice Modify an uint256 param
* @param parameter The name of the parameter modified
* @param data New value for the parameter
*/
function modifyParameters(bytes32 parameter, uint256 data) external isAuthorized {
if (parameter == "surplusAuctionDelay") surplusAuctionDelay = data;
else if (parameter == "surplusTransferDelay") surplusTransferDelay = data;
else if (parameter == "popDebtDelay") popDebtDelay = data;
else if (parameter == "surplusAuctionAmountToSell") surplusAuctionAmountToSell = data;
else if (parameter == "surplusTransferAmount") surplusTransferAmount = data;
else if (parameter == "extraSurplusIsTransferred") extraSurplusIsTransferred = data;
else if (parameter == "debtAuctionBidSize") debtAuctionBidSize = data;
else if (parameter == "initialDebtAuctionMintedTokens") initialDebtAuctionMintedTokens = data;
else if (parameter == "surplusBuffer") surplusBuffer = data;
else if (parameter == "lastSurplusTransferTime") {
require(data > now, "AccountingEngine/invalid-lastSurplusTransferTime");
lastSurplusTransferTime = data;
}
else if (parameter == "lastSurplusAuctionTime") {
require(data > now, "AccountingEngine/invalid-lastSurplusAuctionTime");
lastSurplusAuctionTime = data;
}
else if (parameter == "disableCooldown") disableCooldown = data;
else revert("AccountingEngine/modify-unrecognized-param");
emit ModifyParameters(parameter, data);
}
/**
* @notice Modify an address param
* @param parameter The name of the parameter
* @param data New address for the parameter
*/
function modifyParameters(bytes32 parameter, address data) external isAuthorized {
if (parameter == "surplusAuctionHouse") {
safeEngine.denySAFEModification(address(surplusAuctionHouse));
surplusAuctionHouse = SurplusAuctionHouseLike(data);
safeEngine.approveSAFEModification(data);
}
else if (parameter == "systemStakingPool") {
systemStakingPool = SystemStakingPoolLike(data);
systemStakingPool.canPrintProtocolTokens();
}
else if (parameter == "debtAuctionHouse") debtAuctionHouse = DebtAuctionHouseLike(data);
else if (parameter == "postSettlementSurplusDrain") postSettlementSurplusDrain = data;
else if (parameter == "protocolTokenAuthority") protocolTokenAuthority = ProtocolTokenAuthorityLike(data);
else if (parameter == "extraSurplusReceiver") extraSurplusReceiver = data;
else revert("AccountingEngine/modify-unrecognized-param");
emit ModifyParameters(parameter, data);
}
// --- Getters ---
/*
* @notice Returns the amount of bad debt that is not in the debtQueue and is not currently handled by debt auctions
*/
function unqueuedUnauctionedDebt() public view returns (uint256) {
return subtract(subtract(safeEngine.debtBalance(address(this)), totalQueuedDebt), totalOnAuctionDebt);
}
/*
* @notify Returns a bool indicating whether the AccountingEngine can currently print protocol tokens using debt auctions
*/
function canPrintProtocolTokens() public view returns (bool) {
if (address(systemStakingPool) == address(0)) return true;
try systemStakingPool.canPrintProtocolTokens() returns (bool ok) {
return ok;
} catch(bytes memory) {
return true;
}
}
// --- Debt Queueing ---
/**
* @notice Push bad debt into a queue
* @dev Debt is locked in a queue to give the system enough time to auction collateral
* and gather surplus
* @param debtBlock Amount of debt to push
*/
function pushDebtToQueue(uint256 debtBlock) external isAuthorized {
debtQueue[now] = addition(debtQueue[now], debtBlock);
totalQueuedDebt = addition(totalQueuedDebt, debtBlock);
emit PushDebtToQueue(now, debtQueue[now], totalQueuedDebt);
}
/**
* @notice Pop a block of bad debt from the debt queue
* @dev A block of debt can be popped from the queue after popDebtDelay seconds have passed since it was
* added there
* @param debtBlockTimestamp Timestamp of the block of debt that should be popped out
*/
function popDebtFromQueue(uint256 debtBlockTimestamp) external {
require(addition(debtBlockTimestamp, popDebtDelay) <= now, "AccountingEngine/pop-debt-delay-not-passed");
require(debtQueue[debtBlockTimestamp] > 0, "AccountingEngine/null-debt-block");
totalQueuedDebt = subtract(totalQueuedDebt, debtQueue[debtBlockTimestamp]);
debtPoppers[debtBlockTimestamp] = msg.sender;
emit PopDebtFromQueue(now, debtQueue[debtBlockTimestamp], totalQueuedDebt);
debtQueue[debtBlockTimestamp] = 0;
}
// Debt settlement
/**
* @notice Destroy an equal amount of coins and bad debt
* @dev We can only destroy debt that is not locked in the queue and also not in a debt auction
* @param rad Amount of coins/debt to destroy (number with 45 decimals)
**/
function settleDebt(uint256 rad) public {
require(rad <= safeEngine.coinBalance(address(this)), "AccountingEngine/insufficient-surplus");
require(rad <= unqueuedUnauctionedDebt(), "AccountingEngine/insufficient-debt");
safeEngine.settleDebt(rad);
emit SettleDebt(rad, safeEngine.coinBalance(address(this)), safeEngine.debtBalance(address(this)));
}
/**
* @notice Use surplus coins to destroy debt that was in a debt auction
* @param rad Amount of coins/debt to destroy (number with 45 decimals)
**/
function cancelAuctionedDebtWithSurplus(uint256 rad) external {
require(rad <= totalOnAuctionDebt, "AccountingEngine/not-enough-debt-being-auctioned");
require(rad <= safeEngine.coinBalance(address(this)), "AccountingEngine/insufficient-surplus");
totalOnAuctionDebt = subtract(totalOnAuctionDebt, rad);
safeEngine.settleDebt(rad);
emit CancelAuctionedDebtWithSurplus(rad, totalOnAuctionDebt, safeEngine.coinBalance(address(this)), safeEngine.debtBalance(address(this)));
}
// Debt auction
/**
* @notice Start a debt auction (print protocol tokens in exchange for coins so that the
* system can be recapitalized)
* @dev We can only auction debt that is not already being auctioned and is not locked in the debt queue
**/
function auctionDebt() external returns (uint256 id) {
require(debtAuctionBidSize <= unqueuedUnauctionedDebt(), "AccountingEngine/insufficient-debt");
settleDebt(safeEngine.coinBalance(address(this)));
require(safeEngine.coinBalance(address(this)) == 0, "AccountingEngine/surplus-not-zero");
require(debtAuctionHouse.protocolToken() != address(0), "AccountingEngine/debt-auction-house-null-prot");
require(protocolTokenAuthority.authorizedAccounts(address(debtAuctionHouse)) == 1, "AccountingEngine/debt-auction-house-cannot-print-prot");
require(canPrintProtocolTokens(), "AccountingEngine/staking-pool-denies-printing");
totalOnAuctionDebt = addition(totalOnAuctionDebt, debtAuctionBidSize);
id = debtAuctionHouse.startAuction(address(this), initialDebtAuctionMintedTokens, debtAuctionBidSize);
emit AuctionDebt(id, totalOnAuctionDebt, safeEngine.debtBalance(address(this)));
}
// Surplus auction
/**
* @notice Start a surplus auction
* @dev We can only auction surplus if we wait at least 'surplusAuctionDelay' seconds since the last
* surplus auction trigger, if we keep enough surplus in the buffer and if there is no bad debt left to settle
**/
function auctionSurplus() external returns (uint256 id) {
require(extraSurplusIsTransferred != 1, "AccountingEngine/surplus-transfer-no-auction");
require(surplusAuctionAmountToSell > 0, "AccountingEngine/null-amount-to-auction");
settleDebt(unqueuedUnauctionedDebt());
require(
now >= addition(lastSurplusAuctionTime, surplusAuctionDelay),
"AccountingEngine/surplus-auction-delay-not-passed"
);
require(
safeEngine.coinBalance(address(this)) >=
addition(addition(safeEngine.debtBalance(address(this)), surplusAuctionAmountToSell), surplusBuffer),
"AccountingEngine/insufficient-surplus"
);
require(
unqueuedUnauctionedDebt() == 0,
"AccountingEngine/debt-not-zero"
);
require(surplusAuctionHouse.protocolToken() != address(0), "AccountingEngine/surplus-auction-house-null-prot");
lastSurplusAuctionTime = now;
lastSurplusTransferTime = now;
id = surplusAuctionHouse.startAuction(surplusAuctionAmountToSell, 0);
emit AuctionSurplus(id, lastSurplusAuctionTime, safeEngine.coinBalance(address(this)));
}
// Extra surplus transfers/surplus auction alternative
/**
* @notice Send surplus to an address as an alternative to surplus auctions
* @dev We can only transfer surplus if we wait at least 'surplusTransferDelay' seconds since the last
* transfer, if we keep enough surplus in the buffer and if there is no bad debt left to settle
**/
function transferExtraSurplus() external {
require(extraSurplusIsTransferred == 1, "AccountingEngine/surplus-auction-not-transfer");
require(extraSurplusReceiver != address(0), "AccountingEngine/null-surplus-receiver");
require(surplusTransferAmount > 0, "AccountingEngine/null-amount-to-transfer");
settleDebt(unqueuedUnauctionedDebt());
require(
now >= addition(lastSurplusTransferTime, surplusTransferDelay),
"AccountingEngine/surplus-transfer-delay-not-passed"
);
require(
safeEngine.coinBalance(address(this)) >=
addition(addition(safeEngine.debtBalance(address(this)), surplusTransferAmount), surplusBuffer),
"AccountingEngine/insufficient-surplus"
);
require(
unqueuedUnauctionedDebt() == 0,
"AccountingEngine/debt-not-zero"
);
lastSurplusTransferTime = now;
lastSurplusAuctionTime = now;
safeEngine.transferInternalCoins(address(this), extraSurplusReceiver, surplusTransferAmount);
emit TransferExtraSurplus(extraSurplusReceiver, lastSurplusTransferTime, safeEngine.coinBalance(address(this)));
}
/**
* @notice Disable this contract (normally called by Global Settlement)
* @dev When it's being disabled, the contract will record the current timestamp. Afterwards,
* the contract tries to settle as much debt as possible (if there's any) with any surplus that's
* left in the AccountingEngine
**/
function disableContract() external isAuthorized {
require(contractEnabled == 1, "AccountingEngine/contract-not-enabled");
contractEnabled = 0;
totalQueuedDebt = 0;
totalOnAuctionDebt = 0;
disableTimestamp = now;
surplusAuctionHouse.disableContract();
debtAuctionHouse.disableContract();
safeEngine.settleDebt(minimum(safeEngine.coinBalance(address(this)), safeEngine.debtBalance(address(this))));
emit DisableContract(disableTimestamp, disableCooldown, safeEngine.coinBalance(address(this)), safeEngine.debtBalance(address(this)));
}
/**
* @notice Transfer any remaining surplus after the disable cooldown has passed. Meant to be a backup in case GlobalSettlement.processSAFE
has a bug, governance doesn't have power over the system and there's still surplus left in the AccountingEngine
which then blocks GlobalSettlement.setOutstandingCoinSupply.
* @dev Transfer any remaining surplus after disableCooldown seconds have passed since disabling the contract
**/
function transferPostSettlementSurplus() external {
require(contractEnabled == 0, "AccountingEngine/still-enabled");
require(addition(disableTimestamp, disableCooldown) <= now, "AccountingEngine/cooldown-not-passed");
safeEngine.settleDebt(minimum(safeEngine.coinBalance(address(this)), safeEngine.debtBalance(address(this))));
safeEngine.transferInternalCoins(address(this), postSettlementSurplusDrain, safeEngine.coinBalance(address(this)));
emit TransferPostSettlementSurplus(
postSettlementSurplusDrain,
safeEngine.coinBalance(address(this)),
safeEngine.debtBalance(address(this))
);
}
}