forked from Synthetixio/synthetix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDepot.sol
544 lines (454 loc) · 22.9 KB
/
Depot.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
pragma solidity ^0.5.16;
// Inheritance
import "./Owned.sol";
import "./Pausable.sol";
import "openzeppelin-solidity-2.3.0/contracts/utils/ReentrancyGuard.sol";
import "./MixinResolver.sol";
import "./interfaces/IDepot.sol";
// Libraries
import "./SafeDecimalMath.sol";
// Internal references
import "./interfaces/IERC20.sol";
import "./interfaces/IExchangeRates.sol";
// https://docs.synthetix.io/contracts/source/contracts/depot
contract Depot is Owned, Pausable, ReentrancyGuard, MixinResolver, IDepot {
using SafeMath for uint;
using SafeDecimalMath for uint;
bytes32 internal constant SNX = "SNX";
bytes32 internal constant ETH = "ETH";
/* ========== STATE VARIABLES ========== */
// Address where the ether and Synths raised for selling SNX is transfered to
// Any ether raised for selling Synths gets sent back to whoever deposited the Synths,
// and doesn't have anything to do with this address.
address payable public fundsWallet;
/* Stores deposits from users. */
struct SynthDepositEntry {
// The user that made the deposit
address payable user;
// The amount (in Synths) that they deposited
uint amount;
}
/* User deposits are sold on a FIFO (First in First out) basis. When users deposit
synths with us, they get added this queue, which then gets fulfilled in order.
Conceptually this fits well in an array, but then when users fill an order we
end up copying the whole array around, so better to use an index mapping instead
for gas performance reasons.
The indexes are specified (inclusive, exclusive), so (0, 0) means there's nothing
in the array, and (3, 6) means there are 3 elements at 3, 4, and 5. You can obtain
the length of the "array" by querying depositEndIndex - depositStartIndex. All index
operations use safeAdd, so there is no way to overflow, so that means there is a
very large but finite amount of deposits this contract can handle before it fills up. */
mapping(uint => SynthDepositEntry) public deposits;
// The starting index of our queue inclusive
uint public depositStartIndex;
// The ending index of our queue exclusive
uint public depositEndIndex;
/* This is a convenience variable so users and dApps can just query how much sUSD
we have available for purchase without having to iterate the mapping with a
O(n) amount of calls for something we'll probably want to display quite regularly. */
uint public totalSellableDeposits;
// The minimum amount of sUSD required to enter the FiFo queue
uint public minimumDepositAmount = 50 * SafeDecimalMath.unit();
// A cap on the amount of sUSD you can buy with ETH in 1 transaction
uint public maxEthPurchase = 500 * SafeDecimalMath.unit();
// If a user deposits a synth amount < the minimumDepositAmount the contract will keep
// the total of small deposits which will not be sold on market and the sender
// must call withdrawMyDepositedSynths() to get them back.
mapping(address => uint) public smallDeposits;
/* ========== ADDRESS RESOLVER CONFIGURATION ========== */
bytes32 private constant CONTRACT_SYNTHSUSD = "SynthsUSD";
bytes32 private constant CONTRACT_EXRATES = "ExchangeRates";
bytes32 private constant CONTRACT_SYNTHETIX = "Synthetix";
/* ========== CONSTRUCTOR ========== */
constructor(
address _owner,
address payable _fundsWallet,
address _resolver
) public Owned(_owner) Pausable() MixinResolver(_resolver) {
fundsWallet = _fundsWallet;
}
/* ========== SETTERS ========== */
function setMaxEthPurchase(uint _maxEthPurchase) external onlyOwner {
maxEthPurchase = _maxEthPurchase;
emit MaxEthPurchaseUpdated(maxEthPurchase);
}
/**
* @notice Set the funds wallet where ETH raised is held
* @param _fundsWallet The new address to forward ETH and Synths to
*/
function setFundsWallet(address payable _fundsWallet) external onlyOwner {
fundsWallet = _fundsWallet;
emit FundsWalletUpdated(fundsWallet);
}
/**
* @notice Set the minimum deposit amount required to depoist sUSD into the FIFO queue
* @param _amount The new new minimum number of sUSD required to deposit
*/
function setMinimumDepositAmount(uint _amount) external onlyOwner {
// Do not allow us to set it less than 1 dollar opening up to fractional desposits in the queue again
require(_amount > SafeDecimalMath.unit(), "Minimum deposit amount must be greater than UNIT");
minimumDepositAmount = _amount;
emit MinimumDepositAmountUpdated(minimumDepositAmount);
}
/* ========== MUTATIVE FUNCTIONS ========== */
/**
* @notice Fallback function (exchanges ETH to sUSD)
*/
function() external payable nonReentrant rateNotInvalid(ETH) notPaused {
_exchangeEtherForSynths();
}
/**
* @notice Exchange ETH to sUSD.
*/
/* solhint-disable multiple-sends, reentrancy */
function exchangeEtherForSynths()
external
payable
nonReentrant
rateNotInvalid(ETH)
notPaused
returns (
uint // Returns the number of Synths (sUSD) received
)
{
return _exchangeEtherForSynths();
}
function _exchangeEtherForSynths() internal returns (uint) {
require(msg.value <= maxEthPurchase, "ETH amount above maxEthPurchase limit");
uint ethToSend;
// The multiplication works here because exchangeRates().rateForCurrency(ETH) is specified in
// 18 decimal places, just like our currency base.
uint requestedToPurchase = msg.value.multiplyDecimal(exchangeRates().rateForCurrency(ETH));
uint remainingToFulfill = requestedToPurchase;
// Iterate through our outstanding deposits and sell them one at a time.
for (uint i = depositStartIndex; remainingToFulfill > 0 && i < depositEndIndex; i++) {
SynthDepositEntry memory deposit = deposits[i];
// If it's an empty spot in the queue from a previous withdrawal, just skip over it and
// update the queue. It's already been deleted.
if (deposit.user == address(0)) {
depositStartIndex = depositStartIndex.add(1);
} else {
// If the deposit can more than fill the order, we can do this
// without touching the structure of our queue.
if (deposit.amount > remainingToFulfill) {
// Ok, this deposit can fulfill the whole remainder. We don't need
// to change anything about our queue we can just fulfill it.
// Subtract the amount from our deposit and total.
uint newAmount = deposit.amount.sub(remainingToFulfill);
deposits[i] = SynthDepositEntry({user: deposit.user, amount: newAmount});
totalSellableDeposits = totalSellableDeposits.sub(remainingToFulfill);
// Transfer the ETH to the depositor. Send is used instead of transfer
// so a non payable contract won't block the FIFO queue on a failed
// ETH payable for synths transaction. The proceeds to be sent to the
// synthetix foundation funds wallet. This is to protect all depositors
// in the queue in this rare case that may occur.
ethToSend = remainingToFulfill.divideDecimal(exchangeRates().rateForCurrency(ETH));
// We need to use send here instead of transfer because transfer reverts
// if the recipient is a non-payable contract. Send will just tell us it
// failed by returning false at which point we can continue.
if (!deposit.user.send(ethToSend)) {
fundsWallet.transfer(ethToSend);
emit NonPayableContract(deposit.user, ethToSend);
} else {
emit ClearedDeposit(msg.sender, deposit.user, ethToSend, remainingToFulfill, i);
}
// And the Synths to the recipient.
// Note: Fees are calculated by the Synth contract, so when
// we request a specific transfer here, the fee is
// automatically deducted and sent to the fee pool.
synthsUSD().transfer(msg.sender, remainingToFulfill);
// And we have nothing left to fulfill on this order.
remainingToFulfill = 0;
} else if (deposit.amount <= remainingToFulfill) {
// We need to fulfill this one in its entirety and kick it out of the queue.
// Start by kicking it out of the queue.
// Free the storage because we can.
delete deposits[i];
// Bump our start index forward one.
depositStartIndex = depositStartIndex.add(1);
// We also need to tell our total it's decreased
totalSellableDeposits = totalSellableDeposits.sub(deposit.amount);
// Now fulfill by transfering the ETH to the depositor. Send is used instead of transfer
// so a non payable contract won't block the FIFO queue on a failed
// ETH payable for synths transaction. The proceeds to be sent to the
// synthetix foundation funds wallet. This is to protect all depositors
// in the queue in this rare case that may occur.
ethToSend = deposit.amount.divideDecimal(exchangeRates().rateForCurrency(ETH));
// We need to use send here instead of transfer because transfer reverts
// if the recipient is a non-payable contract. Send will just tell us it
// failed by returning false at which point we can continue.
if (!deposit.user.send(ethToSend)) {
fundsWallet.transfer(ethToSend);
emit NonPayableContract(deposit.user, ethToSend);
} else {
emit ClearedDeposit(msg.sender, deposit.user, ethToSend, deposit.amount, i);
}
// And the Synths to the recipient.
// Note: Fees are calculated by the Synth contract, so when
// we request a specific transfer here, the fee is
// automatically deducted and sent to the fee pool.
synthsUSD().transfer(msg.sender, deposit.amount);
// And subtract the order from our outstanding amount remaining
// for the next iteration of the loop.
remainingToFulfill = remainingToFulfill.sub(deposit.amount);
}
}
}
// Ok, if we're here and 'remainingToFulfill' isn't zero, then
// we need to refund the remainder of their ETH back to them.
if (remainingToFulfill > 0) {
msg.sender.transfer(remainingToFulfill.divideDecimal(exchangeRates().rateForCurrency(ETH)));
}
// How many did we actually give them?
uint fulfilled = requestedToPurchase.sub(remainingToFulfill);
if (fulfilled > 0) {
// Now tell everyone that we gave them that many (only if the amount is greater than 0).
emit Exchange("ETH", msg.value, "sUSD", fulfilled);
}
return fulfilled;
}
/* solhint-enable multiple-sends, reentrancy */
/**
* @notice Exchange ETH to sUSD while insisting on a particular rate. This allows a user to
* exchange while protecting against frontrunning by the contract owner on the exchange rate.
* @param guaranteedRate The exchange rate (ether price) which must be honored or the call will revert.
*/
function exchangeEtherForSynthsAtRate(uint guaranteedRate)
external
payable
rateNotInvalid(ETH)
notPaused
returns (
uint // Returns the number of Synths (sUSD) received
)
{
require(guaranteedRate == exchangeRates().rateForCurrency(ETH), "Guaranteed rate would not be received");
return _exchangeEtherForSynths();
}
function _exchangeEtherForSNX() internal returns (uint) {
// How many SNX are they going to be receiving?
uint synthetixToSend = synthetixReceivedForEther(msg.value);
// Store the ETH in our funds wallet
fundsWallet.transfer(msg.value);
// And send them the SNX.
synthetix().transfer(msg.sender, synthetixToSend);
emit Exchange("ETH", msg.value, "SNX", synthetixToSend);
return synthetixToSend;
}
/**
* @notice Exchange ETH to SNX.
*/
function exchangeEtherForSNX()
external
payable
rateNotInvalid(SNX)
rateNotInvalid(ETH)
notPaused
returns (
uint // Returns the number of SNX received
)
{
return _exchangeEtherForSNX();
}
/**
* @notice Exchange ETH to SNX while insisting on a particular set of rates. This allows a user to
* exchange while protecting against frontrunning by the contract owner on the exchange rates.
* @param guaranteedEtherRate The ether exchange rate which must be honored or the call will revert.
* @param guaranteedSynthetixRate The synthetix exchange rate which must be honored or the call will revert.
*/
function exchangeEtherForSNXAtRate(uint guaranteedEtherRate, uint guaranteedSynthetixRate)
external
payable
rateNotInvalid(SNX)
rateNotInvalid(ETH)
notPaused
returns (
uint // Returns the number of SNX received
)
{
require(guaranteedEtherRate == exchangeRates().rateForCurrency(ETH), "Guaranteed ether rate would not be received");
require(
guaranteedSynthetixRate == exchangeRates().rateForCurrency(SNX),
"Guaranteed synthetix rate would not be received"
);
return _exchangeEtherForSNX();
}
function _exchangeSynthsForSNX(uint synthAmount) internal returns (uint) {
// How many SNX are they going to be receiving?
uint synthetixToSend = synthetixReceivedForSynths(synthAmount);
// Ok, transfer the Synths to our funds wallet.
// These do not go in the deposit queue as they aren't for sale as such unless
// they're sent back in from the funds wallet.
synthsUSD().transferFrom(msg.sender, fundsWallet, synthAmount);
// And send them the SNX.
synthetix().transfer(msg.sender, synthetixToSend);
emit Exchange("sUSD", synthAmount, "SNX", synthetixToSend);
return synthetixToSend;
}
/**
* @notice Exchange sUSD for SNX
* @param synthAmount The amount of synths the user wishes to exchange.
*/
function exchangeSynthsForSNX(uint synthAmount)
external
rateNotInvalid(SNX)
notPaused
returns (
uint // Returns the number of SNX received
)
{
return _exchangeSynthsForSNX(synthAmount);
}
/**
* @notice Exchange sUSD for SNX while insisting on a particular rate. This allows a user to
* exchange while protecting against frontrunning by the contract owner on the exchange rate.
* @param synthAmount The amount of synths the user wishes to exchange.
* @param guaranteedRate A rate (synthetix price) the caller wishes to insist upon.
*/
function exchangeSynthsForSNXAtRate(uint synthAmount, uint guaranteedRate)
external
rateNotInvalid(SNX)
notPaused
returns (
uint // Returns the number of SNX received
)
{
require(guaranteedRate == exchangeRates().rateForCurrency(SNX), "Guaranteed rate would not be received");
return _exchangeSynthsForSNX(synthAmount);
}
/**
* @notice Allows the owner to withdraw SNX from this contract if needed.
* @param amount The amount of SNX to attempt to withdraw (in 18 decimal places).
*/
function withdrawSynthetix(uint amount) external onlyOwner {
synthetix().transfer(owner, amount);
// We don't emit our own events here because we assume that anyone
// who wants to watch what the Depot is doing can
// just watch ERC20 events from the Synth and/or Synthetix contracts
// filtered to our address.
}
/**
* @notice Allows a user to withdraw all of their previously deposited synths from this contract if needed.
* Developer note: We could keep an index of address to deposits to make this operation more efficient
* but then all the other operations on the queue become less efficient. It's expected that this
* function will be very rarely used, so placing the inefficiency here is intentional. The usual
* use case does not involve a withdrawal.
*/
function withdrawMyDepositedSynths() external {
uint synthsToSend = 0;
for (uint i = depositStartIndex; i < depositEndIndex; i++) {
SynthDepositEntry memory deposit = deposits[i];
if (deposit.user == msg.sender) {
// The user is withdrawing this deposit. Remove it from our queue.
// We'll just leave a gap, which the purchasing logic can walk past.
synthsToSend = synthsToSend.add(deposit.amount);
delete deposits[i];
//Let the DApps know we've removed this deposit
emit SynthDepositRemoved(deposit.user, deposit.amount, i);
}
}
// Update our total
totalSellableDeposits = totalSellableDeposits.sub(synthsToSend);
// Check if the user has tried to send deposit amounts < the minimumDepositAmount to the FIFO
// queue which would have been added to this mapping for withdrawal only
synthsToSend = synthsToSend.add(smallDeposits[msg.sender]);
smallDeposits[msg.sender] = 0;
// If there's nothing to do then go ahead and revert the transaction
require(synthsToSend > 0, "You have no deposits to withdraw.");
// Send their deposits back to them (minus fees)
synthsUSD().transfer(msg.sender, synthsToSend);
emit SynthWithdrawal(msg.sender, synthsToSend);
}
/**
* @notice depositSynths: Allows users to deposit synths via the approve / transferFrom workflow
* @param amount The amount of sUSD you wish to deposit (must have been approved first)
*/
function depositSynths(uint amount) external {
// Grab the amount of synths. Will fail if not approved first
synthsUSD().transferFrom(msg.sender, address(this), amount);
// A minimum deposit amount is designed to protect purchasers from over paying
// gas for fullfilling multiple small synth deposits
if (amount < minimumDepositAmount) {
// We cant fail/revert the transaction or send the synths back in a reentrant call.
// So we will keep your synths balance seperate from the FIFO queue so you can withdraw them
smallDeposits[msg.sender] = smallDeposits[msg.sender].add(amount);
emit SynthDepositNotAccepted(msg.sender, amount, minimumDepositAmount);
} else {
// Ok, thanks for the deposit, let's queue it up.
deposits[depositEndIndex] = SynthDepositEntry({user: msg.sender, amount: amount});
emit SynthDeposit(msg.sender, amount, depositEndIndex);
// Walk our index forward as well.
depositEndIndex = depositEndIndex.add(1);
// And add it to our total.
totalSellableDeposits = totalSellableDeposits.add(amount);
}
}
/* ========== VIEWS ========== */
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
addresses = new bytes32[](3);
addresses[0] = CONTRACT_SYNTHSUSD;
addresses[1] = CONTRACT_EXRATES;
addresses[2] = CONTRACT_SYNTHETIX;
}
/**
* @notice Calculate how many SNX you will receive if you transfer
* an amount of synths.
* @param amount The amount of synths (in 18 decimal places) you want to ask about
*/
function synthetixReceivedForSynths(uint amount) public view returns (uint) {
// And what would that be worth in SNX based on the current price?
return amount.divideDecimal(exchangeRates().rateForCurrency(SNX));
}
/**
* @notice Calculate how many SNX you will receive if you transfer
* an amount of ether.
* @param amount The amount of ether (in wei) you want to ask about
*/
function synthetixReceivedForEther(uint amount) public view returns (uint) {
// How much is the ETH they sent us worth in sUSD (ignoring the transfer fee)?
uint valueSentInSynths = amount.multiplyDecimal(exchangeRates().rateForCurrency(ETH));
// Now, how many SNX will that USD amount buy?
return synthetixReceivedForSynths(valueSentInSynths);
}
/**
* @notice Calculate how many synths you will receive if you transfer
* an amount of ether.
* @param amount The amount of ether (in wei) you want to ask about
*/
function synthsReceivedForEther(uint amount) public view returns (uint) {
// How many synths would that amount of ether be worth?
return amount.multiplyDecimal(exchangeRates().rateForCurrency(ETH));
}
/* ========== INTERNAL VIEWS ========== */
function synthsUSD() internal view returns (IERC20) {
return IERC20(requireAndGetAddress(CONTRACT_SYNTHSUSD));
}
function synthetix() internal view returns (IERC20) {
return IERC20(requireAndGetAddress(CONTRACT_SYNTHETIX));
}
function exchangeRates() internal view returns (IExchangeRates) {
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
}
// ========== MODIFIERS ==========
modifier rateNotInvalid(bytes32 currencyKey) {
require(!exchangeRates().rateIsInvalid(currencyKey), "Rate invalid or not a synth");
_;
}
/* ========== EVENTS ========== */
event MaxEthPurchaseUpdated(uint amount);
event FundsWalletUpdated(address newFundsWallet);
event Exchange(string fromCurrency, uint fromAmount, string toCurrency, uint toAmount);
event SynthWithdrawal(address user, uint amount);
event SynthDeposit(address indexed user, uint amount, uint indexed depositIndex);
event SynthDepositRemoved(address indexed user, uint amount, uint indexed depositIndex);
event SynthDepositNotAccepted(address user, uint amount, uint minimum);
event MinimumDepositAmountUpdated(uint amount);
event NonPayableContract(address indexed receiver, uint amount);
event ClearedDeposit(
address indexed fromAddress,
address indexed toAddress,
uint fromETHAmount,
uint toAmount,
uint indexed depositIndex
);
}