/
Crucible.sol
407 lines (340 loc) · 12.7 KB
/
Crucible.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
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.7.6;
pragma abicoder v2;
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/Initializable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {TransferHelper} from "@uniswap/lib/contracts/libraries/TransferHelper.sol";
import {EIP712} from "./EIP712.sol";
import {ERC1271} from "./ERC1271.sol";
import {OwnableERC721} from "./OwnableERC721.sol";
import {IRageQuit} from "../aludel/Aludel.sol";
interface IUniversalVault {
/* user events */
event Locked(address delegate, address token, uint256 amount);
event Unlocked(address delegate, address token, uint256 amount);
event RageQuit(address delegate, address token, bool notified, string reason);
/* data types */
struct LockData {
address delegate;
address token;
uint256 balance;
}
/* initialize function */
function initialize() external;
/* user functions */
function lock(
address token,
uint256 amount,
bytes calldata permission
) external;
function unlock(
address token,
uint256 amount,
bytes calldata permission
) external;
function rageQuit(address delegate, address token)
external
returns (bool notified, string memory error);
function transferERC20(
address token,
address to,
uint256 amount
) external;
function transferETH(address to, uint256 amount) external payable;
/* pure functions */
function calculateLockID(address delegate, address token)
external
pure
returns (bytes32 lockID);
/* getter functions */
function getPermissionHash(
bytes32 eip712TypeHash,
address delegate,
address token,
uint256 amount,
uint256 nonce
) external view returns (bytes32 permissionHash);
function getNonce() external view returns (uint256 nonce);
function owner() external view returns (address ownerAddress);
function getLockSetCount() external view returns (uint256 count);
function getLockAt(uint256 index) external view returns (LockData memory lockData);
function getBalanceDelegated(address token, address delegate)
external
view
returns (uint256 balance);
function getBalanceLocked(address token) external view returns (uint256 balance);
function checkBalances() external view returns (bool validity);
}
/// @title Crucible
/// @notice Vault for isolated storage of staking tokens
/// @dev Warning: not compatible with rebasing tokens
contract Crucible is
IUniversalVault,
EIP712("UniversalVault", "1.0.0"),
ERC1271,
OwnableERC721,
Initializable
{
using SafeMath for uint256;
using Address for address;
using Address for address payable;
using EnumerableSet for EnumerableSet.Bytes32Set;
/* constant */
// Hardcoding a gas limit for rageQuit() is required to prevent gas DOS attacks
// the gas requirement cannot be determined at runtime by querying the delegate
// as it could potentially be manipulated by a malicious delegate who could force
// the calls to revert.
// The gas limit could alternatively be set upon vault initialization or creation
// of a lock, but the gas consumption trade-offs are not favorable.
// Ultimately, to avoid a need for fixed gas limits, the EVM would need to provide
// an error code that allows for reliably catching out-of-gas errors on remote calls.
uint256 public constant RAGEQUIT_GAS = 500000;
bytes32 public constant LOCK_TYPEHASH =
keccak256("Lock(address delegate,address token,uint256 amount,uint256 nonce)");
bytes32 public constant UNLOCK_TYPEHASH =
keccak256("Unlock(address delegate,address token,uint256 amount,uint256 nonce)");
/* storage */
uint256 private _nonce;
mapping(bytes32 => LockData) private _locks;
EnumerableSet.Bytes32Set private _lockSet;
/* initialization function */
function initializeLock() external initializer {}
function initialize() external override initializer {
OwnableERC721._setNFT(msg.sender);
}
/* ether receive */
receive() external payable {}
/* internal overrides */
function _getOwner() internal view override(ERC1271) returns (address ownerAddress) {
return OwnableERC721.owner();
}
/* pure functions */
function calculateLockID(address delegate, address token)
public
pure
override
returns (bytes32 lockID)
{
return keccak256(abi.encodePacked(delegate, token));
}
/* getter functions */
function getPermissionHash(
bytes32 eip712TypeHash,
address delegate,
address token,
uint256 amount,
uint256 nonce
) public view override returns (bytes32 permissionHash) {
return
EIP712._hashTypedDataV4(
keccak256(abi.encode(eip712TypeHash, delegate, token, amount, nonce))
);
}
function getNonce() external view override returns (uint256 nonce) {
return _nonce;
}
function owner()
public
view
override(IUniversalVault, OwnableERC721)
returns (address ownerAddress)
{
return OwnableERC721.owner();
}
function getLockSetCount() external view override returns (uint256 count) {
return _lockSet.length();
}
function getLockAt(uint256 index) external view override returns (LockData memory lockData) {
return _locks[_lockSet.at(index)];
}
function getBalanceDelegated(address token, address delegate)
external
view
override
returns (uint256 balance)
{
return _locks[calculateLockID(delegate, token)].balance;
}
function getBalanceLocked(address token) public view override returns (uint256 balance) {
uint256 count = _lockSet.length();
for (uint256 index; index < count; index++) {
LockData storage _lockData = _locks[_lockSet.at(index)];
if (_lockData.token == token && _lockData.balance > balance)
balance = _lockData.balance;
}
return balance;
}
function checkBalances() external view override returns (bool validity) {
// iterate over all token locks and validate sufficient balance
uint256 count = _lockSet.length();
for (uint256 index; index < count; index++) {
// fetch storage lock reference
LockData storage _lockData = _locks[_lockSet.at(index)];
// if insufficient balance and no∏t shutdown, return false
if (IERC20(_lockData.token).balanceOf(address(this)) < _lockData.balance) return false;
}
// if sufficient balance or shutdown, return true
return true;
}
/* user functions */
/// @notice Lock ERC20 tokens in the vault
/// access control: called by delegate with signed permission from owner
/// state machine: anytime
/// state scope:
/// - insert or update _locks
/// - increase _nonce
/// token transfer: none
/// @param token Address of token being locked
/// @param amount Amount of tokens being locked
/// @param permission Permission signature payload
function lock(
address token,
uint256 amount,
bytes calldata permission
)
external
override
onlyValidSignature(
getPermissionHash(LOCK_TYPEHASH, msg.sender, token, amount, _nonce),
permission
)
{
// get lock id
bytes32 lockID = calculateLockID(msg.sender, token);
// add lock to storage
if (_lockSet.contains(lockID)) {
// if lock already exists, increase amount
_locks[lockID].balance = _locks[lockID].balance.add(amount);
} else {
// if does not exist, create new lock
// add lock to set
assert(_lockSet.add(lockID));
// add lock data to storage
_locks[lockID] = LockData(msg.sender, token, amount);
}
// validate sufficient balance
require(
IERC20(token).balanceOf(address(this)) >= _locks[lockID].balance,
"UniversalVault: insufficient balance"
);
// increase nonce
_nonce += 1;
// emit event
emit Locked(msg.sender, token, amount);
}
/// @notice Unlock ERC20 tokens in the vault
/// access control: called by delegate with signed permission from owner
/// state machine: after valid lock from delegate
/// state scope:
/// - remove or update _locks
/// - increase _nonce
/// token transfer: none
/// @param token Address of token being unlocked
/// @param amount Amount of tokens being unlocked
/// @param permission Permission signature payload
function unlock(
address token,
uint256 amount,
bytes calldata permission
)
external
override
onlyValidSignature(
getPermissionHash(UNLOCK_TYPEHASH, msg.sender, token, amount, _nonce),
permission
)
{
// get lock id
bytes32 lockID = calculateLockID(msg.sender, token);
// validate existing lock
require(_lockSet.contains(lockID), "UniversalVault: missing lock");
// update lock data
if (_locks[lockID].balance > amount) {
// substract amount from lock balance
_locks[lockID].balance = _locks[lockID].balance.sub(amount);
} else {
// delete lock data
delete _locks[lockID];
assert(_lockSet.remove(lockID));
}
// increase nonce
_nonce += 1;
// emit event
emit Unlocked(msg.sender, token, amount);
}
/// @notice Forcibly cancel delegate lock
/// @dev This function will attempt to notify the delegate of the rage quit using
/// a fixed amount of gas.
/// access control: only owner
/// state machine: after valid lock from delegate
/// state scope:
/// - remove item from _locks
/// token transfer: none
/// @param delegate Address of delegate
/// @param token Address of token being unlocked
function rageQuit(address delegate, address token)
external
override
onlyOwner
returns (bool notified, string memory error)
{
// get lock id
bytes32 lockID = calculateLockID(delegate, token);
// validate existing lock
require(_lockSet.contains(lockID), "UniversalVault: missing lock");
// attempt to notify delegate
if (delegate.isContract()) {
// check for sufficient gas
require(gasleft() >= RAGEQUIT_GAS, "UniversalVault: insufficient gas");
// attempt rageQuit notification
try IRageQuit(delegate).rageQuit{gas: RAGEQUIT_GAS}() {
notified = true;
} catch Error(string memory res) {
notified = false;
error = res;
} catch (bytes memory) {
notified = false;
}
}
// update lock storage
assert(_lockSet.remove(lockID));
delete _locks[lockID];
// emit event
emit RageQuit(delegate, token, notified, error);
}
/// @notice Transfer ERC20 tokens out of vault
/// access control: only owner
/// state machine: when balance >= max(lock) + amount
/// state scope: none
/// token transfer: transfer any token
/// @param token Address of token being transferred
/// @param to Address of the recipient
/// @param amount Amount of tokens to transfer
function transferERC20(
address token,
address to,
uint256 amount
) external override onlyOwner {
// check for sufficient balance
require(
IERC20(token).balanceOf(address(this)) >= getBalanceLocked(token).add(amount),
"UniversalVault: insufficient balance"
);
// perform transfer
TransferHelper.safeTransfer(token, to, amount);
}
/// @notice Transfer ERC20 tokens out of vault
/// access control: only owner
/// state machine: when balance >= amount
/// state scope: none
/// token transfer: transfer any token
/// @param to Address of the recipient
/// @param amount Amount of ETH to transfer
function transferETH(address to, uint256 amount) external payable override onlyOwner {
// perform transfer
TransferHelper.safeTransferETH(to, amount);
}
}