-
Notifications
You must be signed in to change notification settings - Fork 110
/
HashedTimelockERC721.sol
260 lines (243 loc) · 8.79 KB
/
HashedTimelockERC721.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
pragma solidity ^0.5.0;
import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";
/**
* @title Hashed Timelock Contracts (HTLCs) on Ethereum ERC721 tokens.
*
* This contract provides a way to create and keep HTLCs for ERC721 tokens.
*
* See HashedTimelock.sol for a contract that provides the same functions
* for the native ETH token.
*
* Protocol:
*
* 1) newContract(receiver, hashlock, timelock, tokenContract, tokenId) - a
* sender calls this to create a new HTLC on a given token (tokenContract)
* for a given token ID. A 32 byte contract id is returned
* 2) withdraw(contractId, preimage) - once the receiver knows the preimage of
* the hashlock hash they can claim the tokens with this function
* 3) refund() - after timelock has expired and if the receiver did not
* withdraw the tokens the sender / creater of the HTLC can get their tokens
* back with this function.
*/
contract HashedTimelockERC721 {
event HTLCERC721New(
bytes32 indexed contractId,
address indexed sender,
address indexed receiver,
address tokenContract,
uint256 tokenId,
bytes32 hashlock,
uint256 timelock
);
event HTLCERC721Withdraw(bytes32 indexed contractId);
event HTLCERC721Refund(bytes32 indexed contractId);
struct LockContract {
address sender;
address receiver;
address tokenContract;
uint256 tokenId;
bytes32 hashlock;
// locked UNTIL this time. Unit depends on consensus algorithm.
// PoA, PoA and IBFT all use seconds. But Quorum Raft uses nano-seconds
uint256 timelock;
bool withdrawn;
bool refunded;
bytes32 preimage;
}
modifier tokensTransferable(address _token, uint256 _tokenId) {
// ensure this contract is approved to transfer the designated token
// so that it is able to honor the claim request later
require(
ERC721(_token).getApproved(_tokenId) == address(this),
"The HTLC must have been designated an approved spender for the tokenId"
);
_;
}
modifier futureTimelock(uint256 _time) {
// only requirement is the timelock time is after the last blocktime (now).
// probably want something a bit further in the future then this.
// but this is still a useful sanity check:
require(_time > now, "timelock time must be in the future");
_;
}
modifier contractExists(bytes32 _contractId) {
require(haveContract(_contractId), "contractId does not exist");
_;
}
modifier hashlockMatches(bytes32 _contractId, bytes32 _x) {
require(
contracts[_contractId].hashlock == sha256(abi.encodePacked(_x)),
"hashlock hash does not match"
);
_;
}
modifier withdrawable(bytes32 _contractId) {
require(contracts[_contractId].receiver == msg.sender, "withdrawable: not receiver");
require(contracts[_contractId].withdrawn == false, "withdrawable: already withdrawn");
// This check needs to be added if claims are allowed after timeout. That is, if the following timelock check is commented out
require(contracts[_contractId].refunded == false, "withdrawable: already refunded");
// if we want to disallow claim to be made after the timeout, uncomment the following line
// require(contracts[_contractId].timelock > now, "withdrawable: timelock time must be in the future");
_;
}
modifier refundable(bytes32 _contractId) {
require(contracts[_contractId].sender == msg.sender, "refundable: not sender");
require(contracts[_contractId].refunded == false, "refundable: already refunded");
require(contracts[_contractId].withdrawn == false, "refundable: already withdrawn");
require(contracts[_contractId].timelock <= now, "refundable: timelock not yet passed");
_;
}
mapping (bytes32 => LockContract) contracts;
/**
* @dev Sender / Payer sets up a new hash time lock contract depositing the
* funds and providing the reciever and terms.
*
* NOTE: _receiver must first call approve() on the token contract.
* See isApprovedOrOwner check in tokensTransferable modifier.
* @param _receiver Receiver of the tokens.
* @param _hashlock A sha-2 sha256 hash hashlock.
* @param _timelock UNIX epoch seconds time that the lock expires at.
* Refunds can be made after this time.
* @param _tokenContract ERC20 Token contract address.
* @param _tokenId Id of the token to lock up.
* @return contractId Id of the new HTLC. This is needed for subsequent
* calls.
*/
function newContract(
address _receiver,
bytes32 _hashlock,
uint256 _timelock,
address _tokenContract,
uint256 _tokenId
)
external
tokensTransferable(_tokenContract, _tokenId)
futureTimelock(_timelock)
returns (bytes32 contractId)
{
contractId = sha256(
abi.encodePacked(
msg.sender,
_receiver,
_tokenContract,
_tokenId,
_hashlock,
_timelock
)
);
// Reject if a contract already exists with the same parameters. The
// sender must change one of these parameters (ideally providing a
// different _hashlock).
if (haveContract(contractId))
revert("Contract already exists");
// This contract becomes the temporary owner of the token
ERC721(_tokenContract).transferFrom(msg.sender, address(this), _tokenId);
contracts[contractId] = LockContract(
msg.sender,
_receiver,
_tokenContract,
_tokenId,
_hashlock,
_timelock,
false,
false,
0x0
);
emit HTLCERC721New(
contractId,
msg.sender,
_receiver,
_tokenContract,
_tokenId,
_hashlock,
_timelock
);
}
/**
* @dev Called by the receiver once they know the preimage of the hashlock.
* This will transfer ownership of the locked tokens to their address.
*
* @param _contractId Id of the HTLC.
* @param _preimage sha256(_preimage) should equal the contract hashlock.
* @return bool true on success
*/
function withdraw(bytes32 _contractId, bytes32 _preimage)
external
contractExists(_contractId)
hashlockMatches(_contractId, _preimage)
withdrawable(_contractId)
returns (bool)
{
LockContract storage c = contracts[_contractId];
c.preimage = _preimage;
c.withdrawn = true;
ERC721(c.tokenContract).transferFrom(address(this), c.receiver, c.tokenId);
emit HTLCERC721Withdraw(_contractId);
return true;
}
/**
* @dev Called by the sender if there was no withdraw AND the time lock has
* expired. This will restore ownership of the tokens to the sender.
*
* @param _contractId Id of HTLC to refund from.
* @return bool true on success
*/
function refund(bytes32 _contractId)
external
contractExists(_contractId)
refundable(_contractId)
returns (bool)
{
LockContract storage c = contracts[_contractId];
c.refunded = true;
ERC721(c.tokenContract).transferFrom(address(this), c.sender, c.tokenId);
emit HTLCERC721Refund(_contractId);
return true;
}
/**
* @dev Get contract details.
* @param _contractId HTLC contract id
* @return All parameters in struct LockContract for _contractId HTLC
*/
function getContract(bytes32 _contractId)
public
view
returns (
address sender,
address receiver,
address tokenContract,
uint256 tokenId,
bytes32 hashlock,
uint256 timelock,
bool withdrawn,
bool refunded,
bytes32 preimage
)
{
if (haveContract(_contractId) == false)
return (address(0), address(0), address(0), 0, 0, 0, false, false, 0);
LockContract storage c = contracts[_contractId];
return (
c.sender,
c.receiver,
c.tokenContract,
c.tokenId,
c.hashlock,
c.timelock,
c.withdrawn,
c.refunded,
c.preimage
);
}
/**
* @dev Is there a contract with id _contractId.
* @param _contractId Id into contracts mapping.
*/
function haveContract(bytes32 _contractId)
internal
view
returns (bool exists)
{
exists = (contracts[_contractId].sender != address(0));
}
}