/
Hololocker.sol
147 lines (135 loc) · 6.41 KB
/
Hololocker.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./HololockerInterface.sol";
contract Hololocker is HololockerInterface, Ownable {
/// Upper limit for lockTime
uint256 public constant MAXIMUM_LOCK_TIME = 1 days;
/// Delay that has to pass from unlocking until NFT can be withdrawn
uint256 public lockTime;
// NFT address => NFT ID => LockInfo
mapping(address => mapping(uint256 => LockInfo)) public nftLockInfo;
error InvalidLockTime();
error NotUnlockedYet();
error Unauthorized();
error UnlockAlreadyRequested();
error InvalidInputArity();
constructor(uint256 lockTime_, address owner_) {
lockTime = lockTime_;
_transferOwnership(owner_);
}
/// @notice Returns `LockInfo` for specified `token => tokenId`
/// @param token NFT tokens contract address
/// @param tokenId NFT tokens identifier
/// @return The `LockInfo` struct information
function getLockInfo(address token, uint256 tokenId) external view returns (LockInfo memory) {
return nftLockInfo[token][tokenId];
}
/// @notice Initiates a lock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Stores a `LockInfo` struct `{owner: owner, operator: msg.sender, unlockTime: 0}` for each `token => tokenId`
/// Emits `Lock` event.
/// Transfers each token:tokenId to this contract.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
/// @param owner NFT tokens owner
function lock(address[] memory tokens, uint256[] memory tokenIds, address owner) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
nftLockInfo[token][tokenId].owner = owner;
nftLockInfo[token][tokenId].operator = msg.sender;
emit Lock(token, owner, tokenId, msg.sender);
IERC721(token).transferFrom(owner, address(this), tokenId);
}
}
/// @notice Requests unlock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is not 0.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// Since only authorized user can use this function, it cannot be used without locking NFT beforehand,
/// because both info.owner and info.operator would be address(0)
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function requestUnlock(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
LockInfo storage info = nftLockInfo[token][tokenId];
if (msg.sender != info.owner && msg.sender != info.operator) {
revert Unauthorized();
}
if (info.unlockTime != 0) {
revert UnlockAlreadyRequested();
}
uint256 unlockTime = block.timestamp + lockTime;
info.unlockTime = unlockTime;
emit Unlock(token, info.owner, tokenId, info.operator, unlockTime);
}
}
/// @notice Withdraws one or more NFTs to their rightful owner
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is
/// either 0 or greater than block.timestamp.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function withdraw(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
LockInfo storage info = nftLockInfo[token][tokenId];
if (msg.sender != info.owner && msg.sender != info.operator) {
revert Unauthorized();
}
if (info.unlockTime == 0 || block.timestamp < info.unlockTime) {
revert NotUnlockedYet();
}
address owner = info.owner;
emit Withdraw(token, info.owner, tokenId, info.operator);
delete nftLockInfo[token][tokenId];
IERC721(token).transferFrom(address(this), owner, tokenId);
}
}
/// @notice Changes `lockTime` variable that is used in `requestUnlock`.
/// @dev Reverts if new value is greater than `MAXIMUM_LOCK_TIME`.
/// Emits `LockTimeUpdate` event.
/// @param newLockTime New lockTime value
function setLockTime(uint256 newLockTime) external onlyOwner {
if (newLockTime > MAXIMUM_LOCK_TIME) {
revert InvalidLockTime();
}
lockTime = newLockTime;
emit LockTimeUpdate(newLockTime);
}
/// @dev From IERC721Receiver, handles initiating a lock upon direct NFT safeTransferFrom function call
/// @param operator The address which called `safeTransferFrom` function
/// @param from The address which previously owned the token
/// @param tokenId The NFT identifier which is being transferred
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata)
external
returns (bytes4)
{
address token = msg.sender;
nftLockInfo[token][tokenId].owner = from;
nftLockInfo[token][tokenId].operator = operator;
emit Lock(token, from, tokenId, operator);
return IERC721Receiver.onERC721Received.selector;
}
}