-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathMixinTicketBrokerCore.sol
349 lines (291 loc) · 12.3 KB
/
MixinTicketBrokerCore.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import "./interfaces/MReserve.sol";
import "./interfaces/MTicketProcessor.sol";
import "./interfaces/MTicketBrokerCore.sol";
import "./MixinContractRegistry.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
abstract contract MixinTicketBrokerCore is MixinContractRegistry, MReserve, MTicketProcessor, MTicketBrokerCore {
using SafeMath for uint256;
struct Sender {
uint256 deposit; // Amount of funds deposited
uint256 withdrawRound; // Round that sender can withdraw deposit & reserve
}
// Mapping of address => Sender
mapping(address => Sender) internal senders;
// Number of rounds before a sender can withdraw after requesting an unlock
uint256 public unlockPeriod;
// Mapping of ticket hashes => boolean indicating if ticket was redeemed
mapping(bytes32 => bool) public usedTickets;
// Checks if msg.value is equal to the given deposit and reserve amounts
modifier checkDepositReserveETHValueSplit(uint256 _depositAmount, uint256 _reserveAmount) {
require(
msg.value == _depositAmount.add(_reserveAmount),
"msg.value does not equal sum of deposit amount and reserve amount"
);
_;
}
// Process deposit funding
modifier processDeposit(address _sender, uint256 _amount) {
Sender storage sender = senders[_sender];
sender.deposit = sender.deposit.add(_amount);
// If the sender themselves funds their deposit, cancel the unlock
if (msg.sender == _sender && _isUnlockInProgress(sender)) {
_cancelUnlock(sender, _sender);
}
_;
emit DepositFunded(_sender, _amount);
}
// Process reserve funding
modifier processReserve(address _sender, uint256 _amount) {
Sender storage sender = senders[_sender];
addReserve(_sender, _amount);
// If the sender themselves funds their reserve, cancel the unlock
if (msg.sender == _sender && _isUnlockInProgress(sender)) {
_cancelUnlock(sender, _sender);
}
_;
}
/**
* @notice Adds ETH to the caller's deposit
*/
function fundDeposit() external payable whenSystemNotPaused processDeposit(msg.sender, msg.value) {
processFunding(msg.value);
}
/**
* @notice Adds ETH to the caller's reserve
*/
function fundReserve() external payable whenSystemNotPaused processReserve(msg.sender, msg.value) {
processFunding(msg.value);
}
/**
* @notice Adds ETH to the caller's deposit and reserve
* @param _depositAmount Amount of ETH to add to the caller's deposit
* @param _reserveAmount Amount of ETH to add to the caller's reserve
*/
function fundDepositAndReserve(uint256 _depositAmount, uint256 _reserveAmount) external payable {
fundDepositAndReserveFor(msg.sender, _depositAmount, _reserveAmount);
}
/**
* @notice Adds ETH to the address' deposit and reserve
* @param _depositAmount Amount of ETH to add to the address' deposit
* @param _reserveAmount Amount of ETH to add to the address' reserve
*/
function fundDepositAndReserveFor(
address _addr,
uint256 _depositAmount,
uint256 _reserveAmount
)
public
payable
whenSystemNotPaused
checkDepositReserveETHValueSplit(_depositAmount, _reserveAmount)
processDeposit(_addr, _depositAmount)
processReserve(_addr, _reserveAmount)
{
processFunding(msg.value);
}
/**
* @notice Redeems a winning ticket that has been signed by a sender and reveals the
recipient recipientRand that corresponds to the recipientRandHash included in the ticket
* @param _ticket Winning ticket to be redeemed in order to claim payment
* @param _sig Sender's signature over the hash of `_ticket`
* @param _recipientRand The preimage for the recipientRandHash included in `_ticket`
*/
function redeemWinningTicket(
Ticket memory _ticket,
bytes memory _sig,
uint256 _recipientRand
) public whenSystemNotPaused currentRoundInitialized {
bytes32 ticketHash = getTicketHash(_ticket);
// Require a valid winning ticket for redemption
requireValidWinningTicket(_ticket, ticketHash, _sig, _recipientRand);
Sender storage sender = senders[_ticket.sender];
// Require sender to be locked
require(isLocked(sender), "sender is unlocked");
// Require either a non-zero deposit or non-zero reserve for the sender
require(sender.deposit > 0 || remainingReserve(_ticket.sender) > 0, "sender deposit and reserve are zero");
// Mark ticket as used to prevent replay attacks involving redeeming
// the same winning ticket multiple times
usedTickets[ticketHash] = true;
uint256 amountToTransfer;
if (_ticket.faceValue > sender.deposit) {
// If ticket face value > sender's deposit then claim from
// the sender's reserve
amountToTransfer = sender.deposit.add(
claimFromReserve(_ticket.sender, _ticket.recipient, _ticket.faceValue.sub(sender.deposit))
);
sender.deposit = 0;
} else {
// If ticket face value <= sender's deposit then only deduct
// from sender's deposit
amountToTransfer = _ticket.faceValue;
sender.deposit = sender.deposit.sub(_ticket.faceValue);
}
if (amountToTransfer > 0) {
winningTicketTransfer(_ticket.recipient, amountToTransfer, _ticket.auxData);
emit WinningTicketTransfer(_ticket.sender, _ticket.recipient, amountToTransfer);
}
emit WinningTicketRedeemed(
_ticket.sender,
_ticket.recipient,
_ticket.faceValue,
_ticket.winProb,
_ticket.senderNonce,
_recipientRand,
_ticket.auxData
);
}
/**
* @notice Initiates the unlock period for the caller
*/
function unlock() public whenSystemNotPaused {
Sender storage sender = senders[msg.sender];
require(sender.deposit > 0 || remainingReserve(msg.sender) > 0, "sender deposit and reserve are zero");
require(!_isUnlockInProgress(sender), "unlock already initiated");
uint256 currentRound = roundsManager().currentRound();
sender.withdrawRound = currentRound.add(unlockPeriod);
emit Unlock(msg.sender, currentRound, sender.withdrawRound);
}
/**
* @notice Cancels the unlock period for the caller
*/
function cancelUnlock() public whenSystemNotPaused {
Sender storage sender = senders[msg.sender];
_cancelUnlock(sender, msg.sender);
}
/**
* @notice Withdraws all ETH from the caller's deposit and reserve
*/
function withdraw() public whenSystemNotPaused {
Sender storage sender = senders[msg.sender];
uint256 deposit = sender.deposit;
uint256 reserve = remainingReserve(msg.sender);
require(deposit > 0 || reserve > 0, "sender deposit and reserve are zero");
require(_isUnlockInProgress(sender), "no unlock request in progress");
require(!isLocked(sender), "account is locked");
sender.deposit = 0;
clearReserve(msg.sender);
withdrawTransfer(payable(msg.sender), deposit.add(reserve));
emit Withdrawal(msg.sender, deposit, reserve);
}
/**
* @notice Returns whether a sender is currently in the unlock period
* @param _sender Address of sender
* @return Boolean indicating whether `_sender` has an unlock in progress
*/
function isUnlockInProgress(address _sender) public view returns (bool) {
Sender memory sender = senders[_sender];
return _isUnlockInProgress(sender);
}
/**
* @notice Returns info about a sender
* @param _sender Address of sender
* @return sender Info about the sender for `_sender`
* @return reserve Info about the reserve for `_sender`
*/
function getSenderInfo(address _sender) public view returns (Sender memory sender, ReserveInfo memory reserve) {
sender = senders[_sender];
reserve = getReserveInfo(_sender);
}
/**
* @dev Returns the hash of a ticket
* @param _ticket Ticket to be hashed
* @return keccak256 hash of `_ticket`
*/
function getTicketHash(Ticket memory _ticket) public pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
_ticket.recipient,
_ticket.sender,
_ticket.faceValue,
_ticket.winProb,
_ticket.senderNonce,
_ticket.recipientRandHash,
_ticket.auxData
)
);
}
/**
* @dev Helper to cancel an unlock
* @param _sender Sender that is cancelling an unlock
* @param _senderAddress Address of sender
*/
function _cancelUnlock(Sender storage _sender, address _senderAddress) internal {
require(_isUnlockInProgress(_sender), "no unlock request in progress");
_sender.withdrawRound = 0;
emit UnlockCancelled(_senderAddress);
}
/**
* @dev Validates a winning ticket, succeeds or reverts
* @param _ticket Winning ticket to be validated
* @param _ticketHash Hash of `_ticket`
* @param _sig Sender's signature over `_ticketHash`
* @param _recipientRand The preimage for the recipientRandHash included in `_ticket`
*/
function requireValidWinningTicket(
Ticket memory _ticket,
bytes32 _ticketHash,
bytes memory _sig,
uint256 _recipientRand
) internal view {
require(_ticket.recipient != address(0), "ticket recipient is null address");
require(_ticket.sender != address(0), "ticket sender is null address");
requireValidTicketAuxData(_ticket.auxData);
require(
keccak256(abi.encodePacked(_recipientRand)) == _ticket.recipientRandHash,
"recipientRand does not match recipientRandHash"
);
require(!usedTickets[_ticketHash], "ticket is used");
require(isValidTicketSig(_ticket.sender, _sig, _ticketHash), "invalid signature over ticket hash");
require(isWinningTicket(_sig, _recipientRand, _ticket.winProb), "ticket did not win");
}
/**
* @dev Returns whether a sender is locked
* @param _sender Sender to check for locked status
* @return Boolean indicating whether sender is currently locked
*/
function isLocked(Sender memory _sender) internal view returns (bool) {
return _sender.withdrawRound == 0 || roundsManager().currentRound() < _sender.withdrawRound;
}
/**
* @dev Returns whether a signature over a ticket hash is valid for a sender
* @param _sender Address of sender
* @param _sig Signature over `_ticketHash`
* @param _ticketHash Hash of the ticket
* @return Boolean indicating whether `_sig` is valid signature over `_ticketHash` for `_sender`
*/
function isValidTicketSig(
address _sender,
bytes memory _sig,
bytes32 _ticketHash
) internal pure returns (bool) {
require(_sig.length == 65, "INVALID_SIGNATURE_LENGTH");
address signer = ECDSA.recover(ECDSA.toEthSignedMessageHash(_ticketHash), _sig);
return signer != address(0) && _sender == signer;
}
/**
* @dev Returns whether a ticket won
* @param _sig Sender's signature over the ticket
* @param _recipientRand The preimage for the recipientRandHash included in the ticket
* @param _winProb The winning probability of the ticket
* @return Boolean indicating whether the ticket won
*/
function isWinningTicket(
bytes memory _sig,
uint256 _recipientRand,
uint256 _winProb
) internal pure returns (bool) {
return uint256(keccak256(abi.encodePacked(_sig, _recipientRand))) < _winProb;
}
/**
* @dev Helper to check if a sender is currently in the unlock period
* @param _sender Sender to check for an unlock
* @return Boolean indicating whether the sender is currently in the unlock period
*/
function _isUnlockInProgress(Sender memory _sender) internal pure returns (bool) {
return _sender.withdrawRound > 0;
}
}