-
Notifications
You must be signed in to change notification settings - Fork 4
/
XDEFIDistribution.sol
347 lines (257 loc) · 14 KB
/
XDEFIDistribution.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
// SPDX-License-Identifier: MIT
pragma solidity =0.8.10;
import { ERC721, ERC721Enumerable, Strings } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IEIP2612 } from "./interfaces/IEIP2612.sol";
import { IXDEFIDistribution } from "./interfaces/IXDEFIDistribution.sol";
/// @dev Handles distributing XDEFI to NFTs that have locked up XDEFI for various durations of time.
contract XDEFIDistribution is IXDEFIDistribution, ERC721Enumerable {
uint88 internal MAX_TOTAL_XDEFI_SUPPLY = uint88(240_000_000_000_000_000_000_000_000);
// See https://github.com/ethereum/EIPs/issues/1726#issuecomment-472352728
uint256 internal constant _pointsMultiplier = uint256(2**128);
uint256 internal _pointsPerUnit;
address public immutable XDEFI;
uint256 public distributableXDEFI;
uint256 public totalDepositedXDEFI;
uint256 public totalUnits;
mapping(uint256 => Position) public positionOf;
mapping(uint256 => uint8) public bonusMultiplierOf; // Scaled by 100 (i.e. 1.1x is 110, 2.55x is 255).
uint256 internal immutable _zeroDurationPointBase;
string public baseURI;
address public owner;
address public pendingOwner;
uint256 internal _locked;
constructor (address XDEFI_, string memory baseURI_, uint256 zeroDurationPointBase_) ERC721("Locked XDEFI", "lXDEFI") {
require((XDEFI = XDEFI_) != address(0), "INVALID_TOKEN");
owner = msg.sender;
baseURI = baseURI_;
_zeroDurationPointBase = zeroDurationPointBase_;
}
modifier onlyOwner() {
require(owner == msg.sender, "NOT_OWNER");
_;
}
modifier noReenter() {
require(_locked == 0, "LOCKED");
_locked = uint256(1);
_;
_locked = uint256(0);
}
/*******************/
/* Admin Functions */
/*******************/
function acceptOwnership() external {
require(pendingOwner == msg.sender, "NOT_PENDING_OWNER");
emit OwnershipAccepted(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
function proposeOwnership(address newOwner_) external onlyOwner {
emit OwnershipProposed(owner, pendingOwner = newOwner_);
}
function setBaseURI(string memory baseURI_) external onlyOwner {
baseURI = baseURI_;
}
function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external onlyOwner {
uint256 count = durations_.length;
for (uint256 i; i < count; ++i) {
uint256 duration = durations_[i];
require(duration <= uint256(18250 days), "INVALID_DURATION");
emit LockPeriodSet(duration, bonusMultiplierOf[duration] = multipliers[i]);
}
}
/**********************/
/* Position Functions */
/**********************/
function lock(uint256 amount_, uint256 duration_, address destination_) external noReenter returns (uint256 tokenId_) {
// Lock the XDEFI in the contract.
SafeERC20.safeTransferFrom(IERC20(XDEFI), msg.sender, address(this), amount_);
// Handle the lock position creation and get the tokenId of the locked position.
return _lock(amount_, duration_, destination_);
}
function lockWithPermit(uint256 amount_, uint256 duration_, address destination_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_) external noReenter returns (uint256 tokenId_) {
// Approve this contract for the amount, using the provided signature.
IEIP2612(XDEFI).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_);
// Lock the XDEFI in the contract.
SafeERC20.safeTransferFrom(IERC20(XDEFI), msg.sender, address(this), amount_);
// Handle the lock position creation and get the tokenId of the locked position.
return _lock(amount_, duration_, destination_);
}
function relock(uint256 tokenId_, uint256 lockAmount_, uint256 duration_, address destination_) external noReenter returns (uint256 amountUnlocked_, uint256 newTokenId_) {
// Handle the unlock and get the amount of XDEFI eligible to withdraw.
amountUnlocked_ = _unlock(msg.sender, tokenId_);
// Throw convenient error if trying to re-lock more than was unlocked. `amountUnlocked_ - lockAmount_` would have reverted below anyway.
require(lockAmount_ <= amountUnlocked_, "INSUFFICIENT_AMOUNT_UNLOCKED");
// Handle the lock position creation and get the tokenId of the locked position.
newTokenId_ = _lock(lockAmount_, duration_, destination_);
uint256 withdrawAmount = amountUnlocked_ - lockAmount_;
if (withdrawAmount != uint256(0)) {
// Send the excess XDEFI to the destination, if needed.
SafeERC20.safeTransfer(IERC20(XDEFI), destination_, withdrawAmount);
}
// NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlock`) and transferring out.
_updateXDEFIBalance();
}
function unlock(uint256 tokenId_, address destination_) external noReenter returns (uint256 amountUnlocked_) {
// Handle the unlock and get the amount of XDEFI eligible to withdraw.
amountUnlocked_ = _unlock(msg.sender, tokenId_);
// Send the the unlocked XDEFI to the destination.
SafeERC20.safeTransfer(IERC20(XDEFI), destination_, amountUnlocked_);
// NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlock`) and transferring out.
_updateXDEFIBalance();
}
function updateDistribution() external {
uint256 totalUnitsCached = totalUnits;
require(totalUnitsCached > uint256(0), "NO_UNIT_SUPPLY");
uint256 newXDEFI = _toUint256Safe(_updateXDEFIBalance());
if (newXDEFI == uint256(0)) return;
_pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached);
emit DistributionUpdated(msg.sender, newXDEFI);
}
function withdrawableOf(uint256 tokenId_) public view returns (uint256 withdrawableXDEFI_) {
Position storage position = positionOf[tokenId_];
return _withdrawableGiven(position.units, position.depositedXDEFI, position.pointsCorrection);
}
/****************************/
/* Batch Position Functions */
/****************************/
function relockBatch(uint256[] memory tokenIds_, uint256 lockAmount_, uint256 duration_, address destination_) external noReenter returns (uint256 amountUnlocked_, uint256 newTokenId_) {
// Handle the unlocks and get the amount of XDEFI eligible to withdraw.
amountUnlocked_ = _unlockBatch(msg.sender, tokenIds_);
// Throw convenient error if trying to re-lock more than was unlocked. `amountUnlocked_ - lockAmount_` would have reverted below anyway.
require(lockAmount_ <= amountUnlocked_, "INSUFFICIENT_AMOUNT_UNLOCKED");
// Handle the lock position creation and get the tokenId of the locked position.
newTokenId_ = _lock(lockAmount_, duration_, destination_);
uint256 withdrawAmount = amountUnlocked_ - lockAmount_;
if (withdrawAmount != uint256(0)) {
// Send the excess XDEFI to the destination, if needed.
SafeERC20.safeTransfer(IERC20(XDEFI), destination_, withdrawAmount);
}
// NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlockBatch`) and transferring out.
_updateXDEFIBalance();
}
function unlockBatch(uint256[] memory tokenIds_, address destination_) external noReenter returns (uint256 amountUnlocked_) {
// Handle the unlocks and get the amount of XDEFI eligible to withdraw.
amountUnlocked_ = _unlockBatch(msg.sender, tokenIds_);
// Send the the unlocked XDEFI to the destination.
SafeERC20.safeTransfer(IERC20(XDEFI), destination_, amountUnlocked_);
// NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlockBatch`) and transferring out.
_updateXDEFIBalance();
}
/*****************/
/* NFT Functions */
/*****************/
function getPoints(uint256 amount_, uint256 duration_) external view returns (uint256 points_) {
return _getPoints(amount_, duration_);
}
function merge(uint256[] memory tokenIds_, address destination_) external returns (uint256 tokenId_) {
uint256 count = tokenIds_.length;
require(count > uint256(1), "MIN_2_TO_MERGE");
uint256 points;
// For each NFT, check that it belongs to the caller, burn it, and accumulate the points.
for (uint256 i; i < count; ++i) {
uint256 tokenId = tokenIds_[i];
require(ownerOf(tokenId) == msg.sender, "NOT_OWNER");
require(positionOf[tokenId].expiry == uint32(0), "POSITION_NOT_UNLOCKED");
_burn(tokenId);
points += _getPointsFromTokenId(tokenId);
}
// Mine a new NFT to the destinations, based on the accumulated points.
_safeMint(destination_, tokenId_ = _generateNewTokenId(points));
}
function pointsOf(uint256 tokenId_) external view returns (uint256 points_) {
require(_exists(tokenId_), "NO_TOKEN");
return _getPointsFromTokenId(tokenId_);
}
function tokenURI(uint256 tokenId_) public view override(IXDEFIDistribution, ERC721) returns (string memory tokenURI_) {
require(_exists(tokenId_), "NO_TOKEN");
return string(abi.encodePacked(baseURI, Strings.toString(tokenId_)));
}
/**********************/
/* Internal Functions */
/**********************/
function _generateNewTokenId(uint256 points_) internal view returns (uint256 tokenId_) {
// Points is capped at 128 bits (max supply of XDEFI for 10 years locked), total supply of NFTs is capped at 128 bits.
return (points_ << uint256(128)) + uint128(totalSupply() + 1);
}
function _getPoints(uint256 amount_, uint256 duration_) internal view returns (uint256 points_) {
return amount_ * (duration_ + _zeroDurationPointBase);
}
function _getPointsFromTokenId(uint256 tokenId_) internal pure returns (uint256 points_) {
return tokenId_ >> uint256(128);
}
function _lock(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) {
// Prevent locking 0 amount in order generate many score-less NFTs, even if it is inefficient, and such NFTs would be ignored.
require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, "INVALID_AMOUNT");
// Get bonus multiplier and check that it is not zero (which validates the duration).
uint8 bonusMultiplier = bonusMultiplierOf[duration_];
require(bonusMultiplier != uint8(0), "INVALID_DURATION");
// Mint a locked staked position NFT to the destination.
_safeMint(destination_, tokenId_ = _generateNewTokenId(_getPoints(amount_, duration_)));
// Track deposits.
totalDepositedXDEFI += amount_;
// Create Position.
uint96 units = uint96((amount_ * uint256(bonusMultiplier)) / uint256(100));
totalUnits += units;
positionOf[tokenId_] =
Position({
units: units,
depositedXDEFI: uint88(amount_),
expiry: uint32(block.timestamp + duration_),
created: uint32(block.timestamp),
bonusMultiplier: bonusMultiplier,
pointsCorrection: -_toInt256Safe(_pointsPerUnit * units)
});
emit LockPositionCreated(tokenId_, destination_, amount_, duration_);
}
function _toInt256Safe(uint256 x_) internal pure returns (int256 y_) {
y_ = int256(x_);
assert(y_ >= int256(0));
}
function _toUint256Safe(int256 x_) internal pure returns (uint256 y_) {
assert(x_ >= int256(0));
return uint256(x_);
}
function _unlock(address account_, uint256 tokenId_) internal returns (uint256 amountUnlocked_) {
// Check that the account is the position NFT owner.
require(ownerOf(tokenId_) == account_, "NOT_OWNER");
// Fetch position.
Position storage position = positionOf[tokenId_];
uint96 units = position.units;
uint88 depositedXDEFI = position.depositedXDEFI;
uint32 expiry = position.expiry;
// Check that enough time has elapsed in order to unlock.
require(expiry != uint32(0), "NO_LOCKED_POSITION");
require(block.timestamp >= uint256(expiry), "CANNOT_UNLOCK");
// Get the withdrawable amount of XDEFI for the position.
amountUnlocked_ = _withdrawableGiven(units, depositedXDEFI, position.pointsCorrection);
// Track deposits.
totalDepositedXDEFI -= uint256(depositedXDEFI);
// Burn FDT Position.
totalUnits -= units;
delete positionOf[tokenId_];
emit LockPositionWithdrawn(tokenId_, account_, amountUnlocked_);
}
function _unlockBatch(address account_, uint256[] memory tokenIds_) internal returns (uint256 amountUnlocked_) {
uint256 count = tokenIds_.length;
require(count > uint256(1), "USE_UNLOCK");
// Handle the unlock for each position and accumulate the unlocked amount.
for (uint256 i; i < count; ++i) {
amountUnlocked_ += _unlock(account_, tokenIds_[i]);
}
}
function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) {
uint256 previousDistributableXDEFI = distributableXDEFI;
uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI;
return _toInt256Safe(currentDistributableXDEFI) - _toInt256Safe(previousDistributableXDEFI);
}
function _withdrawableGiven(uint96 units_, uint88 depositedXDEFI_, int256 pointsCorrection_) internal view returns (uint256 withdrawableXDEFI_) {
return
(
_toUint256Safe(
_toInt256Safe(_pointsPerUnit * uint256(units_)) +
pointsCorrection_
) / _pointsMultiplier
) + uint256(depositedXDEFI_);
}
}