Vaults
Discover the different contracts of our vesting and locking engine.
Locklet is a multi-chain platform, our logic is therefore transcribed in various languages and contracts, themselves deployed on different blockchains.

Source code

All of our contracts are open-source and therefore available on our Github. These are also systematically audited by reputable security companies, these audit reports are available on this dedicated page or on QuillHash's Github.
Ethereum Virtual Machine (EVM)
Neo Virtual Machine (NeoVM)
1
// contracts/LockletTokenVault.sol
2
// SPDX-License-Identifier: No License
3
​
4
pragma solidity 0.8.3;
5
​
6
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
7
import "@openzeppelin/contracts/utils/math/SignedSafeMath.sol";
8
import "@openzeppelin/contracts/utils/Address.sol";
9
import "@openzeppelin/contracts/access/AccessControl.sol";
10
import "@openzeppelin/contracts/security/Pausable.sol";
11
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
12
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
13
​
14
import "./LockletToken.sol";
15
​
16
contract LockletTokenVault is AccessControl, Pausable, ReentrancyGuard {
17
using SafeMath for uint256;
18
using SignedSafeMath for int256;
19
​
20
bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
21
​
22
address public lockletTokenAddress;
23
​
24
struct RecipientCallData {
25
address recipientAddress;
26
uint256 amount;
27
}
28
​
29
struct Recipient {
30
address recipientAddress;
31
uint256 amount;
32
uint256 daysClaimed;
33
uint256 amountClaimed;
34
bool isActive;
35
}
36
​
37
struct Lock {
38
uint256 creationTime;
39
address tokenAddress;
40
uint256 startTime;
41
uint256 durationInDays;
42
address initiatorAddress;
43
bool isRevocable;
44
bool isRevoked;
45
bool isActive;
46
}
47
​
48
struct LockWithRecipients {
49
uint256 index;
50
Lock lock;
51
Recipient[] recipients;
52
}
53
​
54
uint256 private _nextLockIndex;
55
Lock[] private _locks;
56
mapping(uint256 => Recipient[]) private _locksRecipients;
57
​
58
mapping(address => uint256[]) private _initiatorsLocksIndexes;
59
mapping(address => uint256[]) private _recipientsLocksIndexes;
60
​
61
mapping(address => mapping(address => uint256)) private _refunds;
62
​
63
address private _stakersRedisAddress;
64
address private _foundationRedisAddress;
65
bool private _isDeprecated;
66
​
67
// #region Governance Variables
68
​
69
uint256 private _creationFlatFeeLktAmount;
70
uint256 private _revocationFlatFeeLktAmount;
71
​
72
uint256 private _creationPercentFee;
73
​
74
// #endregion
75
​
76
// #region Events
77
​
78
event LockAdded(uint256 indexed lockIndex);
79
event LockedTokensClaimed(uint256 indexed lockIndex, address indexed recipientAddress, uint256 claimedAmount);
80
event LockRevoked(uint256 indexed lockIndex, uint256 unlockedAmount, uint256 remainingLockedAmount);
81
event LockRefundPulled(address indexed recipientAddress, address indexed tokenAddress, uint256 refundedAmount);
82
​
83
// #endregion
84
​
85
constructor(address lockletTokenAddr) {
86
lockletTokenAddress = lockletTokenAddr;
87
​
88
_nextLockIndex = 0;
89
​
90
_stakersRedisAddress = address(0);
91
_foundationRedisAddress = 0x25Bd291bE258E90e7A0648aC5c690555aA9e8930;
92
_isDeprecated = false;
93
​
94
_creationFlatFeeLktAmount = 0;
95
_revocationFlatFeeLktAmount = 0;
96
_creationPercentFee = 0;
97
​
98
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
99
_setupRole(GOVERNOR_ROLE, msg.sender);
100
}
101
​
102
function addLock(
103
address tokenAddress,
104
uint256 totalAmount,
105
uint256 cliffInDays,
106
uint256 durationInDays,
107
RecipientCallData[] calldata recipientsData,
108
bool isRevocable,
109
bool payFeesWithLkt
110
) external nonReentrant whenNotPaused contractNotDeprecated {
111
require(Address.isContract(tokenAddress), "LockletTokenVault: Token address is not a contract");
112
ERC20 token = ERC20(tokenAddress);
113
​
114
require(totalAmount > 0, "LockletTokenVault: The total amount is equal to zero");
115
​
116
if (payFeesWithLkt) {
117
LockletToken lktToken = lockletToken();
118
​
119
if (_creationFlatFeeLktAmount > 0) {
120
require(lktToken.balanceOf(msg.sender) >= _creationFlatFeeLktAmount, "LockletTokenVault: Not enough LKT to pay fees");
121
require(lktToken.transferFrom(msg.sender, address(this), _creationFlatFeeLktAmount));
122
​
123
uint256 burnAmount = _creationFlatFeeLktAmount.mul(45).div(100);
124
uint256 stakersRedisAmount = _creationFlatFeeLktAmount.mul(45).div(100);
125
uint256 foundationRedisAmount = _creationFlatFeeLktAmount.mul(10).div(100);
126
​
127
require(lktToken.burn(burnAmount));
128
require(lktToken.transfer(_stakersRedisAddress, stakersRedisAmount));
129
require(lktToken.transfer(_foundationRedisAddress, foundationRedisAmount));
130
}
131
​
132
require(token.balanceOf(msg.sender) >= totalAmount, "LockletTokenVault: Token insufficient balance");
133
require(token.transferFrom(msg.sender, address(this), totalAmount));
134
} else {
135
uint256 creationPercentFeeAmount = 0;
136
if (_creationPercentFee > 0) {
137
creationPercentFeeAmount = totalAmount.mul(_creationPercentFee).div(10000);
138
}
139
​
140
uint256 totalAmountWithFees = totalAmount.add(creationPercentFeeAmount);
141
​
142
require(token.balanceOf(msg.sender) >= totalAmountWithFees, "LockletTokenVault: Token insufficient balance");
143
require(token.transferFrom(msg.sender, address(this), totalAmountWithFees));
144
​
145
if (creationPercentFeeAmount > 0) {
146
uint256 stakersRedisAmount = creationPercentFeeAmount.mul(90).div(100);
147
uint256 foundationRedisAmount = creationPercentFeeAmount.mul(10).div(100);
148
​
149
require(token.transfer(_stakersRedisAddress, stakersRedisAmount));
150
require(token.transfer(_foundationRedisAddress, foundationRedisAmount));
151
}
152
}
153
​
154
uint256 lockIndex = _nextLockIndex;
155
_nextLockIndex = _nextLockIndex.add(1);
156
​
157
Lock memory lock = Lock({
158
creationTime: blockTime(),
159
tokenAddress: tokenAddress,
160
startTime: blockTime().add(cliffInDays * 1 days),
161
durationInDays: durationInDays,
162
initiatorAddress: msg.sender,
163
isRevocable: durationInDays > 1 ? isRevocable : false,
164
isRevoked: false,
165
isActive: true
166
});
167
​
168
_locks.push(lock);
169
_initiatorsLocksIndexes[msg.sender].push(lockIndex);
170
​
171
uint256 totalAmountCheck = 0;
172
​
173
for (uint256 i = 0; i < recipientsData.length; i++) {
174
RecipientCallData calldata recipientData = recipientsData[i];
175
​
176
uint256 unlockedAmountPerDay = recipientData.amount.div(durationInDays);
177
require(unlockedAmountPerDay > 0, "LockletTokenVault: The unlocked amount per day is equal to zero");
178
​
179
totalAmountCheck = totalAmountCheck.add(recipientData.amount);
180
​
181
Recipient memory recipient = Recipient({
182
recipientAddress: recipientData.recipientAddress,
183
amount: recipientData.amount,
184
daysClaimed: 0,
185
amountClaimed: 0,
186
isActive: true
187
});
188
​
189
_recipientsLocksIndexes[recipientData.recipientAddress].push(lockIndex);
190
_locksRecipients[lockIndex].push(recipient);
191
}
192
​
193
require(totalAmountCheck == totalAmount, "LockletTokenVault: The calculated total amount is not equal to the actual total amount");
194
​
195
emit LockAdded(lockIndex);
196
}
197
​
198
function claimLockedTokens(uint256 lockIndex) external nonReentrant whenNotPaused {
199
Lock storage lock = _locks[lockIndex];
200
require(lock.isActive == true, "LockletTokenVault: Lock not existing");
201
require(lock.isRevoked == false, "LockletTokenVault: This lock has been revoked");
202
​
203
Recipient[] storage recipients = _locksRecipients[lockIndex];
204
​
205
int256 recipientIndex = getRecipientIndexByAddress(recipients, msg.sender);
206
require(recipientIndex != -1, "LockletTokenVault: Forbidden");
207
​
208
Recipient storage recipient = recipients[uint256(recipientIndex)];
209
​
210
uint256 daysVested;
211
uint256 unlockedAmount;
212
(daysVested, unlockedAmount) = calculateClaim(lock, recipient);
213
require(unlockedAmount > 0, "LockletTokenVault: The amount of unlocked tokens is equal to zero");
214
​
215
recipient.daysClaimed = recipient.daysClaimed.add(daysVested);
216
recipient.amountClaimed = recipient.amountClaimed.add(unlockedAmount);
217
​
218
ERC20 token = ERC20(lock.tokenAddress);
219
​
220
require(token.transfer(recipient.recipientAddress, unlockedAmount), "LockletTokenVault: Unlocked tokens transfer failed");
221
emit LockedTokensClaimed(lockIndex, recipient.recipientAddress, unlockedAmount);
222
}
223
​
224
function revokeLock(uint256 lockIndex) external nonReentrant whenNotPaused {
225
Lock storage lock = _locks[lockIndex];
226
require(lock.isActive == true, "LockletTokenVault: Lock not existing");
227
require(lock.initiatorAddress == msg.sender, "LockletTokenVault: Forbidden");
228
require(lock.isRevocable == true, "LockletTokenVault: Lock not revocable");
229
require(lock.isRevoked == false, "LockletTokenVault: This lock has already been revoked");
230
​
231
lock.isRevoked = true;
232
​
233
if (_revocationFlatFeeLktAmount > 0) {
234
LockletToken lktToken = lockletToken();
235
require(lktToken.balanceOf(msg.sender) >= _revocationFlatFeeLktAmount, "LockletTokenVault: Not enough LKT to pay fees");
236
require(lktToken.transferFrom(msg.sender, address(this), _revocationFlatFeeLktAmount));
237
​
238
uint256 burnAmount = _revocationFlatFeeLktAmount.mul(45).div(100);
239
uint256 stakersRedisAmount = _revocationFlatFeeLktAmount.mul(45).div(100);
240
uint256 foundationRedisAmount = _revocationFlatFeeLktAmount.mul(10).div(100);
241
​
242
require(lktToken.burn(burnAmount));
243
require(lktToken.transfer(_stakersRedisAddress, stakersRedisAmount));
244
require(lktToken.transfer(_foundationRedisAddress, foundationRedisAmount));
245
}
246
​
247
Recipient[] storage recipients = _locksRecipients[lockIndex];
248
​
249
address tokenAddr = lock.tokenAddress;
250
address initiatorAddr = lock.initiatorAddress;
251
​
252
uint256 totalAmount = 0;
253
uint256 totalUnlockedAmount = 0;
254
​
255
for (uint256 i = 0; i < recipients.length; i++) {
256
Recipient storage recipient = recipients[i];
257
​
258
totalAmount = totalAmount.add(recipient.amount);
259
​
260
uint256 daysVested;
261
uint256 unlockedAmount;
262
(daysVested, unlockedAmount) = calculateClaim(lock, recipient);
263
​
264
if (unlockedAmount > 0) {
265
address recipientAddr = recipient.recipientAddress;
266
_refunds[recipientAddr][tokenAddr] = _refunds[recipientAddr][tokenAddr].add(unlockedAmount);
267
}
268
​
269
totalUnlockedAmount = totalUnlockedAmount.add(recipient.amountClaimed.add(unlockedAmount));
270
}
271
​
272
uint256 totalLockedAmount = totalAmount.sub(totalUnlockedAmount);
273
_refunds[initiatorAddr][tokenAddr] = _refunds[initiatorAddr][tokenAddr].add(totalLockedAmount);
274
​
275
emit LockRevoked(lockIndex, totalUnlockedAmount, totalLockedAmount);
276
}
277
​
278
function pullRefund(address tokenAddress) external nonReentrant whenNotPaused {
279
uint256 refundAmount = getRefundAmount(tokenAddress);
280
require(refundAmount > 0, "LockletTokenVault: No refund found for this token");
281
​
282
_refunds[msg.sender][tokenAddress] = 0;
283
​
284
ERC20 token = ERC20(tokenAddress);
285
require(token.transfer(msg.sender, refundAmount), "LockletTokenVault: Refund tokens transfer failed");
286
​
287
emit LockRefundPulled(msg.sender, tokenAddress, refundAmount);
288
}
289
​
290
// #region Views
291
​
292
function getLock(uint256 lockIndex) public view returns (LockWithRecipients memory) {
293
Lock storage lock = _locks[lockIndex];
294
require(lock.isActive == true, "LockletTokenVault: Lock not existing");
295
​
296
return LockWithRecipients({index: lockIndex, lock: lock, recipients: _locksRecipients[lockIndex]});
297
}
298
​
299
function getLocksLength() public view returns (uint256) {
300
return _locks.length;
301
}
302
​
303
function getLocks(int256 page, int256 pageSize) public view returns (LockWithRecipients[] memory) {
304
require(getLocksLength() > 0, "LockletTokenVault: There is no lock");
305
​
306
int256 queryStartLockIndex = int256(getLocksLength()).sub(pageSize.mul(page)).add(pageSize).sub(1);
307
require(queryStartLockIndex >= 0, "LockletTokenVault: Out of bounds");
308
​
309
int256 queryEndLockIndex = queryStartLockIndex.sub(pageSize).add(1);
310
if (queryEndLockIndex < 0) {
311
queryEndLockIndex = 0;
312
}
313
​
314
int256 currentLockIndex = queryStartLockIndex;
315
require(uint256(currentLockIndex) <= getLocksLength().sub(1), "LockletTokenVault: Out of bounds");
316
​
317
LockWithRecipients[] memory results = new LockWithRecipients[](uint256(pageSize));
318
uint256 index = 0;
319
​
320
for (currentLockIndex; currentLockIndex >= queryEndLockIndex; currentLockIndex--) {
321
uint256 currentLockIndexAsUnsigned = uint256(currentLockIndex);
322
if (currentLockIndexAsUnsigned <= getLocksLength().sub(1)) {
323
results[index] = getLock(currentLockIndexAsUnsigned);
324
}
325
​
326
index++;
327
}
328
​
329
return results;
330
}
331
​
332
function getLocksByInitiator(address initiatorAddress) public view returns (LockWithRecipients[] memory) {
333
uint256 initiatorLocksLength = _initiatorsLocksIndexes[initiatorAddress].length;
334
require(initiatorLocksLength > 0, "LockletTokenVault: The initiator has no lock");
335
​
336
LockWithRecipients[] memory results = new LockWithRecipients[](initiatorLocksLength);
337
​
338
for (uint256 index = 0; index < initiatorLocksLength; index++) {
339
uint256 lockIndex = _initiatorsLocksIndexes[initiatorAddress][index];
340
results[index] = getLock(lockIndex);
341
}
342
​
343
return results;
344
}
345
​
346
function getLocksByRecipient(address recipientAddress) public view returns (LockWithRecipients[] memory) {
347
uint256 recipientLocksLength = _recipientsLocksIndexes[recipientAddress].length;
348
require(recipientLocksLength > 0, "LockletTokenVault: The recipient has no lock");
349
​
350
LockWithRecipients[] memory results = new LockWithRecipients[](recipientLocksLength);
351
​
352
for (uint256 index = 0; index < recipientLocksLength; index++) {
353
uint256 lockIndex = _recipientsLocksIndexes[recipientAddress][index];
354
results[index] = getLock(lockIndex);
355
}
356
​
357
return results;
358
}
359
​
360
function getRefundAmount(address tokenAddress) public view returns (uint256) {
361
return _refunds[msg.sender][tokenAddress];
362
}
363
​
364
function getClaimByLockAndRecipient(uint256 lockIndex, address recipientAddress) public view returns (uint256, uint256) {
365
Lock storage lock = _locks[lockIndex];
366
require(lock.isActive == true, "LockletTokenVault: Lock not existing");
367
​
368
Recipient[] storage recipients = _locksRecipients[lockIndex];
369
​
370
int256 recipientIndex = getRecipientIndexByAddress(recipients, recipientAddress);
371
require(recipientIndex != -1, "LockletTokenVault: Forbidden");
372
​
373
Recipient storage recipient = recipients[uint256(recipientIndex)];
374
​
375
uint256 daysVested;
376
uint256 unlockedAmount;
377
(daysVested, unlockedAmount) = calculateClaim(lock, recipient);
378
​
379
return (daysVested, unlockedAmount);
380
}
381
​
382
function getCreationFlatFeeLktAmount() public view returns (uint256) {
383
return _creationFlatFeeLktAmount;
384
}
385
​
386
function getRevocationFlatFeeLktAmount() public view returns (uint256) {
387
return _revocationFlatFeeLktAmount;
388
}
389
​
390
function getCreationPercentFee() public view returns (uint256) {
391
return _creationPercentFee;
392
}
393
​
394
function isDeprecated() public view returns (bool) {
395
return _isDeprecated;
396
}
397
​
398
function getRecipientIndexByAddress(Recipient[] storage recipients, address recipientAddress) private view returns (int256) {
399
int256 recipientIndex = -1;
400
for (uint256 i = 0; i < recipients.length; i++) {
401
if (recipients[i].recipientAddress == recipientAddress) {
402
recipientIndex = int256(i);
403
break;
404
}
405
}
406
return recipientIndex;
407
}
408
​
409
function calculateClaim(Lock storage lock, Recipient storage recipient) private view returns (uint256, uint256) {
410
require(recipient.amountClaimed < recipient.amount, "LockletTokenVault: The recipient has already claimed the maximum amount");
411
​
412
if (blockTime() < lock.startTime) {
413
return (0, 0);
414
}
415
​
416
// check if cliff has reached
417
uint256 elapsedDays = blockTime().sub(lock.startTime).div(1 days);
418
​
419
if (elapsedDays >= lock.durationInDays) {
420
// if over duration, all tokens vested
421
uint256 remainingAmount = recipient.amount.sub(recipient.amountClaimed);
422
return (lock.durationInDays, remainingAmount);
423
} else {
424
uint256 daysVested = elapsedDays.sub(recipient.daysClaimed);
425
uint256 unlockedAmountPerDay = recipient.amount.div(lock.durationInDays);
426
uint256 unlockedAmount = daysVested.mul(unlockedAmountPerDay);
427
return (daysVested, unlockedAmount);
428
}
429
}
430
​
431
function blockTime() private view returns (uint256) {
432
return block.timestamp;
433
}
434
​
435
function lockletToken() private view returns (LockletToken) {
436
return LockletToken(lockletTokenAddress);
437
}
438
​
439
// #endregion
440
​
441
// #region Governance
442
​
443
function setCreationFlatFeeLktAmount(uint256 amount) external onlyGovernor {
444
require(amount >= 0, "LockletTokenVault: Invalid value");
445
_creationFlatFeeLktAmount = amount;
446
}
447
​
448
function setRevocationFlatFeeLktAmount(uint256 amount) external onlyGovernor {
449
require(amount >= 0, "LockletTokenVault: Invalid value");
450
_revocationFlatFeeLktAmount = amount;
451
}
452
​
453
function setCreationPercentFee(uint256 amount) external onlyGovernor {
454
require(amount >= 0 && amount <= 10000, "LockletTokenVault: Invalid value");
455
_creationPercentFee = amount;
456
}
457
​
458
function setStakersRedisAddress(address addr) external onlyGovernor {
459
require(addr != address(0), "LockletTokenVault: Invalid value");
460
_stakersRedisAddress = addr;
461
}
462
​
463
function pause() external onlyGovernor {
464
_pause();
465
}
466
​
467
function unpause() external onlyGovernor {
468
_unpause();
469
}
470
​
471
function setDeprecated(bool deprecated) external onlyGovernor {
472
_isDeprecated = deprecated;
473
}
474
​
475
// #endregion
476
​
477
// #region Modifiers
478
​
479
modifier onlyGovernor() {
480
require(hasRole(GOVERNOR_ROLE, msg.sender), "LockletTokenVault: Caller is not a GOVERNOR");
481
_;
482
}
483
​
484
modifier contractNotDeprecated() {
485
require(!_isDeprecated, "LockletTokenVault: This version of the contract is deprecated");
486
_;
487
}
488
​
489
// #endregion
490
}
Copied!
1
// βŒ› Coming soon
Copied!

Token contract addresses

Blockchain
Token Contract Address
Status
Ethereum
βœ… Deployed
Binance Smart Chain
βœ… Deployed
Huobi ECO Chain
N/A
βŒ› Coming soon
Tron
N/A
βŒ› Coming soon
NEO (N3)
N/A
βŒ› Coming soon
Last modified 2mo ago