/
MixinKeys.sol
414 lines (377 loc) · 10.9 KB
/
MixinKeys.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './MixinLockCore.sol';
/**
* @title Mixin for managing `Key` data, as well as the * Approval related functions needed to meet the ERC721
* standard.
* @author HardlyDifficult
* @dev `Mixins` are a design pattern seen in the 0x contracts. It simply
* separates logically groupings of code to ease readability.
*/
contract MixinKeys is
MixinLockCore
{
// The struct for a key
struct Key {
uint tokenId;
uint expirationTimestamp;
}
// Emitted when the Lock owner expires a user's Key
event ExpireKey(uint indexed tokenId);
// Emitted when the expiration of a key is modified
event ExpirationChanged(
uint indexed _tokenId,
uint _amount,
bool _timeAdded
);
event KeyManagerChanged(uint indexed _tokenId, address indexed _newManager);
// Keys
// Each owner can have at most exactly one key
// TODO: could we use public here? (this could be confusing though because it getter will
// return 0 values when missing a key)
mapping (address => Key) internal keyByOwner;
// Each tokenId can have at most exactly one owner at a time.
// Returns 0 if the token does not exist
// TODO: once we decouple tokenId from owner address (incl in js), then we can consider
// merging this with totalSupply into an array instead.
mapping (uint => address) internal _ownerOf;
// Addresses of owners are also stored in an array.
// Addresses are never removed by design to avoid abuses around referals
address[] public owners;
// A given key has both an owner and a manager.
// If keyManager == address(0) then the key owner is also the manager
// Each key can have at most 1 keyManager.
mapping (uint => address) public keyManagerOf;
// Keeping track of approved transfers
// This is a mapping of addresses which have approved
// the transfer of a key to another address where their key can be transferred
// Note: the approver may actually NOT have a key... and there can only
// be a single approved address
mapping (uint => address) private approved;
// Keeping track of approved operators for a given Key manager.
// This approves a given operator for all keys managed by the calling "keyManager"
// The caller may not currently be the keyManager for ANY keys.
// These approvals are never reset/revoked automatically, unlike "approved",
// which is reset on transfer.
mapping (address => mapping (address => bool)) private managerToOperatorApproved;
// Ensure that the caller is the keyManager of the key
// or that the caller has been approved
// for ownership of that key
modifier onlyKeyManagerOrApproved(
uint _tokenId
)
{
require(
_isKeyManager(_tokenId, msg.sender) ||
_isApproved(_tokenId, msg.sender) ||
isApprovedForAll(_ownerOf[_tokenId], msg.sender),
'ONLY_KEY_MANAGER_OR_APPROVED'
);
_;
}
// Ensures that an owner owns or has owned a key in the past
modifier ownsOrHasOwnedKey(
address _keyOwner
) {
require(
keyByOwner[_keyOwner].expirationTimestamp > 0, 'HAS_NEVER_OWNED_KEY'
);
_;
}
// Ensures that an owner has a valid key
modifier hasValidKey(
address _user
) {
require(
getHasValidKey(_user), 'KEY_NOT_VALID'
);
_;
}
// Ensures that a key has an owner
modifier isKey(
uint _tokenId
) {
require(
_ownerOf[_tokenId] != address(0), 'NO_SUCH_KEY'
);
_;
}
// Ensure that the caller owns the key
modifier onlyKeyOwner(
uint _tokenId
) {
require(
ownerOf(_tokenId) == msg.sender, 'ONLY_KEY_OWNER'
);
_;
}
/**
* In the specific case of a Lock, each owner can own only at most 1 key.
* @return The number of NFTs owned by `_keyOwner`, either 0 or 1.
*/
function balanceOf(
address _keyOwner
)
public
view
returns (uint)
{
require(_keyOwner != address(0), 'INVALID_ADDRESS');
return getHasValidKey(_keyOwner) ? 1 : 0;
}
/**
* Checks if the user has a non-expired key.
*/
function getHasValidKey(
address _keyOwner
)
public
view
returns (bool)
{
return keyByOwner[_keyOwner].expirationTimestamp > block.timestamp;
}
/**
* @notice Find the tokenId for a given user
* @return The tokenId of the NFT, else returns 0
*/
function getTokenIdFor(
address _account
) public view
returns (uint)
{
return keyByOwner[_account].tokenId;
}
/**
* @dev Returns the key's ExpirationTimestamp field for a given owner.
* @param _keyOwner address of the user for whom we search the key
* @dev Returns 0 if the owner has never owned a key for this lock
*/
function keyExpirationTimestampFor(
address _keyOwner
) public view
returns (uint)
{
return keyByOwner[_keyOwner].expirationTimestamp;
}
/**
* Public function which returns the total number of unique owners (both expired
* and valid). This may be larger than totalSupply.
*/
function numberOfOwners()
public
view
returns (uint)
{
return owners.length;
}
// Returns the owner of a given tokenId
function ownerOf(
uint _tokenId
) public view
returns(address)
{
return _ownerOf[_tokenId];
}
/**
* @notice Public function for updating transfer and cancel rights for a given key
* @param _tokenId The id of the key to assign rights for
* @param _keyManager The address with the manager's rights for the given key.
* Setting _keyManager to address(0) means the keyOwner is also the keyManager
*/
function setKeyManagerOf(
uint _tokenId,
address _keyManager
) public
isKey(_tokenId)
{
require(
_isKeyManager(_tokenId, msg.sender) ||
isLockManager(msg.sender),
'UNAUTHORIZED_KEY_MANAGER_UPDATE'
);
_setKeyManagerOf(_tokenId, _keyManager);
}
function _setKeyManagerOf(
uint _tokenId,
address _keyManager
) internal
{
if(keyManagerOf[_tokenId] != _keyManager) {
keyManagerOf[_tokenId] = _keyManager;
_clearApproval(_tokenId);
emit KeyManagerChanged(_tokenId, address(0));
}
}
/**
* This approves _approved to get ownership of _tokenId.
* Note: that since this is used for both purchase and transfer approvals
* the approved token may not exist.
*/
function approve(
address _approved,
uint _tokenId
)
public
onlyIfAlive
onlyKeyManagerOrApproved(_tokenId)
{
require(msg.sender != _approved, 'APPROVE_SELF');
approved[_tokenId] = _approved;
emit Approval(_ownerOf[_tokenId], _approved, _tokenId);
}
/**
* @notice Get the approved address for a single NFT
* @dev Throws if `_tokenId` is not a valid NFT.
* @param _tokenId The NFT to find the approved address for
* @return The approved address for this NFT, or the zero address if there is none
*/
function getApproved(
uint _tokenId
) public view
isKey(_tokenId)
returns (address)
{
address approvedRecipient = approved[_tokenId];
return approvedRecipient;
}
/**
* @dev Tells whether an operator is approved by a given keyManager
* @param _owner owner address which you want to query the approval of
* @param _operator operator address which you want to query the approval of
* @return bool whether the given operator is approved by the given owner
*/
function isApprovedForAll(
address _owner,
address _operator
) public view
returns (bool)
{
uint tokenId = keyByOwner[_owner].tokenId;
address keyManager = keyManagerOf[tokenId];
if(keyManager == address(0)) {
return managerToOperatorApproved[_owner][_operator];
} else {
return managerToOperatorApproved[keyManager][_operator];
}
}
/**
* Returns true if _keyManager is the manager of the key
* identified by _tokenId
*/
function _isKeyManager(
uint _tokenId,
address _keyManager
) internal view
returns (bool)
{
if(keyManagerOf[_tokenId] == _keyManager ||
(keyManagerOf[_tokenId] == address(0) && ownerOf(_tokenId) == _keyManager)) {
return true;
} else {
return false;
}
}
/**
* Assigns the key a new tokenId (from totalSupply) if it does not already have
* one assigned.
*/
function _assignNewTokenId(
Key storage _key
) internal
{
if (_key.tokenId == 0) {
// This is a brand new owner
// We increment the tokenId counter
_totalSupply++;
// we assign the incremented `_totalSupply` as the tokenId for the new key
_key.tokenId = _totalSupply;
}
}
/**
* Records the owner of a given tokenId
*/
function _recordOwner(
address _keyOwner,
uint _tokenId
) internal
{
if (ownerOf(_tokenId) != _keyOwner) {
// TODO: this may include duplicate entries
owners.push(_keyOwner);
// We register the owner of the tokenID
_ownerOf[_tokenId] = _keyOwner;
}
}
/**
* @notice Modify the expirationTimestamp of a key
* by a given amount.
* @param _tokenId The ID of the key to modify.
* @param _deltaT The amount of time in seconds by which
* to modify the keys expirationTimestamp
* @param _addTime Choose whether to increase or decrease
* expirationTimestamp (false == decrease, true == increase)
* @dev Throws if owner does not have a valid key.
*/
function _timeMachine(
uint _tokenId,
uint256 _deltaT,
bool _addTime
) internal
{
address tokenOwner = ownerOf(_tokenId);
require(tokenOwner != address(0), 'NON_EXISTENT_KEY');
Key storage key = keyByOwner[tokenOwner];
uint formerTimestamp = key.expirationTimestamp;
bool validKey = getHasValidKey(tokenOwner);
if(_addTime) {
if(validKey) {
key.expirationTimestamp = formerTimestamp + _deltaT;
} else {
key.expirationTimestamp = block.timestamp + _deltaT;
}
} else {
key.expirationTimestamp = formerTimestamp - _deltaT;
}
emit ExpirationChanged(_tokenId, _deltaT, _addTime);
}
/**
* @dev Sets or unsets the approval of a given operator
* An operator is allowed to transfer all tokens of the sender on their behalf
* @param _to operator address to set the approval
* @param _approved representing the status of the approval to be set
*/
function setApprovalForAll(
address _to,
bool _approved
) public
onlyIfAlive
{
require(_to != msg.sender, 'APPROVE_SELF');
managerToOperatorApproved[msg.sender][_to] = _approved;
emit ApprovalForAll(msg.sender, _to, _approved);
}
/**
* @dev Checks if the given user is approved to transfer the tokenId.
*/
function _isApproved(
uint _tokenId,
address _user
) internal view
returns (bool)
{
return approved[_tokenId] == _user;
}
/**
* @dev Function to clear current approval of a given token ID
* @param _tokenId uint256 ID of the token to be transferred
*/
function _clearApproval(
uint256 _tokenId
) internal
{
if (approved[_tokenId] != address(0)) {
approved[_tokenId] = address(0);
}
}
}