/
BondNFT.sol
383 lines (352 loc) · 13 KB
/
BondNFT.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract BondNFT is ERC721Enumerable, Ownable {
uint constant private DAY = 24 * 60 * 60;
struct Bond {
uint id;
address owner;
address asset;
uint amount;
uint mintEpoch;
uint mintTime;
uint expireEpoch;
uint pending;
uint shares;
uint period;
bool expired;
}
mapping(address => uint256) public epoch;
uint private totalBonds;
string public baseURI;
address public manager;
address[] public assets;
mapping(address => bool) public allowedAsset;
mapping(address => uint) private assetsIndex;
mapping(uint256 => mapping(address => uint256)) private bondPaid;
mapping(address => mapping(uint256 => uint256)) private accRewardsPerShare; // tigAsset => epoch => accRewardsPerShare
mapping(uint => Bond) private _idToBond;
mapping(address => uint) public totalShares;
mapping(address => mapping(address => uint)) public userDebt; // user => tigAsset => amount
constructor(
string memory _setBaseURI,
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
baseURI = _setBaseURI;
}
/**
* @notice Create a bond
* @dev Should only be called by a manager contract
* @param _asset tigAsset token to lock
* @param _amount tigAsset amount
* @param _period time to lock for in days
* @param _owner address to receive the bond
* @return id ID of the minted bond
*/
function createLock(
address _asset,
uint _amount,
uint _period,
address _owner
) external onlyManager() returns(uint id) {
require(allowedAsset[_asset], "!Asset");
unchecked {
uint shares = _amount * _period / 365;
uint expireEpoch = epoch[_asset] + _period;
id = ++totalBonds;
totalShares[_asset] += shares;
Bond memory _bond = Bond(
id, // id
address(0), // owner
_asset, // tigAsset token
_amount, // tigAsset amount
epoch[_asset], // mint epoch
block.timestamp,// mint timestamp
expireEpoch, // expire epoch
0, // pending
shares, // linearly scaling share of rewards
_period, // lock period
false // is expired boolean
);
_idToBond[id] = _bond;
_mint(_owner, _bond);
}
emit Lock(_asset, _amount, _period, _owner, id);
}
/**
* @notice Extend the lock period and/or amount of a bond
* @dev Should only be called by a manager contract
* @param _id ID of the bond
* @param _asset tigAsset token address
* @param _amount amount of tigAsset being added
* @param _period days being added to the bond
* @param _sender address extending the bond
*/
function extendLock(
uint _id,
address _asset,
uint _amount,
uint _period,
address _sender
) external onlyManager() {
Bond memory bond = idToBond(_id);
Bond storage _bond = _idToBond[_id];
require(bond.owner == _sender, "!owner");
require(!bond.expired, "Expired");
require(bond.asset == _asset, "!BondAsset");
require(bond.pending == 0);
require(epoch[bond.asset] == block.timestamp/DAY, "Bad epoch");
require(bond.period+_period <= 365, "MAX PERIOD");
unchecked {
uint shares = (bond.amount + _amount) * (bond.period + _period) / 365;
uint expireEpoch = block.timestamp/DAY + bond.period + _period;
totalShares[bond.asset] += shares-bond.shares;
_bond.shares = shares;
_bond.amount += _amount;
_bond.expireEpoch = expireEpoch;
_bond.period += _period;
_bond.mintTime = block.timestamp;
_bond.mintEpoch = epoch[bond.asset];
bondPaid[_id][bond.asset] = accRewardsPerShare[bond.asset][epoch[bond.asset]] * _bond.shares / 1e18;
}
emit ExtendLock(_period, _amount, _sender, _id);
}
/**
* @notice Release a bond
* @dev Should only be called by a manager contract
* @param _id ID of the bond
* @param _releaser address initiating the release of the bond
* @return amount amount of tigAsset returned
* @return lockAmount amount of tigAsset locked in the bond
* @return asset tigAsset token released
* @return _owner bond owner
*/
function release(
uint _id,
address _releaser
) external onlyManager() returns(uint amount, uint lockAmount, address asset, address _owner) {
Bond memory bond = idToBond(_id);
require(bond.expired, "!expire");
if (_releaser != bond.owner) {
unchecked {
require(bond.expireEpoch + 7 < epoch[bond.asset], "Bond owner priority");
}
}
amount = bond.amount;
unchecked {
totalShares[bond.asset] -= bond.shares;
(uint256 _claimAmount,) = claim(_id, bond.owner);
amount += _claimAmount;
}
asset = bond.asset;
lockAmount = bond.amount;
_owner = bond.owner;
_burn(_id);
emit Release(asset, lockAmount, _owner, _id);
}
/**
* @notice Claim rewards from a bond
* @dev Should only be called by a manager contract
* @param _id ID of the bond to claim rewards from
* @param _claimer address claiming rewards
* @return amount amount of tigAsset claimed
* @return tigAsset tigAsset token address
*/
function claim(
uint _id,
address _claimer
) public onlyManager() returns(uint amount, address tigAsset) {
Bond memory bond = idToBond(_id);
require(_claimer == bond.owner, "!owner");
amount = bond.pending;
tigAsset = bond.asset;
unchecked {
if (bond.expired) {
uint _pendingDelta = (bond.shares * accRewardsPerShare[bond.asset][epoch[bond.asset]] / 1e18 - bondPaid[_id][bond.asset]) - (bond.shares * accRewardsPerShare[bond.asset][bond.expireEpoch-1] / 1e18 - bondPaid[_id][bond.asset]);
if (totalShares[bond.asset] > 0) {
accRewardsPerShare[bond.asset][epoch[bond.asset]] += _pendingDelta*1e18/totalShares[bond.asset];
}
}
bondPaid[_id][bond.asset] += amount;
}
IERC20(tigAsset).transfer(manager, amount);
emit ClaimFees(tigAsset, amount, _claimer, _id);
}
/**
* @notice Claim user debt left from bond transfer
* @dev Should only be called by a manager contract
* @param _user user address
* @param _tigAsset tigAsset token address
* @return amount amount of tigAsset claimed
*/
function claimDebt(
address _user,
address _tigAsset
) public onlyManager() returns(uint amount) {
amount = userDebt[_user][_tigAsset];
userDebt[_user][_tigAsset] = 0;
IERC20(_tigAsset).transfer(manager, amount);
emit ClaimDebt(_tigAsset, amount, _user);
}
/**
* @notice Distribute rewards to bonds
* @param _tigAsset tigAsset token address
* @param _amount tigAsset amount
*/
function distribute(
address _tigAsset,
uint _amount
) external {
if (totalShares[_tigAsset] == 0 || !allowedAsset[_tigAsset]) return;
IERC20(_tigAsset).transferFrom(_msgSender(), address(this), _amount);
unchecked {
uint aEpoch = block.timestamp / DAY;
if (aEpoch > epoch[_tigAsset]) {
for (uint i=epoch[_tigAsset]; i<aEpoch; i++) {
epoch[_tigAsset] += 1;
accRewardsPerShare[_tigAsset][i+1] = accRewardsPerShare[_tigAsset][i];
}
}
accRewardsPerShare[_tigAsset][aEpoch] += _amount * 1e18 / totalShares[_tigAsset];
}
emit Distribution(_tigAsset, _amount);
}
/**
* @notice Get all data for a bond
* @param _id ID of the bond
* @return bond Bond object
*/
function idToBond(uint256 _id) public view returns (Bond memory bond) {
bond = _idToBond[_id];
bond.owner = ownerOf(_id);
bond.expired = bond.expireEpoch <= epoch[bond.asset] ? true : false;
unchecked {
uint _accRewardsPerShare = accRewardsPerShare[bond.asset][bond.expired ? bond.expireEpoch-1 : epoch[bond.asset]];
bond.pending = bond.shares * _accRewardsPerShare / 1e18 - bondPaid[_id][bond.asset];
}
}
/*
* @notice Get expired boolean for a bond
* @param _id ID of the bond
* @return bool true if bond is expired
*/
function isExpired(uint256 _id) public view returns (bool) {
Bond memory bond = _idToBond[_id];
return bond.expireEpoch <= epoch[bond.asset] ? true : false;
}
/*
* @notice Get pending rewards for a bond
* @param _id ID of the bond
* @return bool true if bond is expired
*/
function pending(
uint256 _id
) public view returns (uint256) {
return idToBond(_id).pending;
}
function totalAssets() public view returns (uint256) {
return assets.length;
}
/*
* @notice Gets an array of all whitelisted token addresses
* @return address array of addresses
*/
function getAssets() public view returns (address[] memory) {
return assets;
}
function _baseURI() internal override view returns (string memory) {
return baseURI;
}
function safeTransferMany(address _to, uint[] calldata _ids) external {
unchecked {
for (uint i=0; i<_ids.length; i++) {
_transfer(_msgSender(), _to, _ids[i]);
}
}
}
function safeTransferFromMany(address _from, address _to, uint[] calldata _ids) external {
unchecked {
for (uint i=0; i<_ids.length; i++) {
safeTransferFrom(_from, _to, _ids[i]);
}
}
}
function approveMany(address _to, uint[] calldata _ids) external {
unchecked {
for (uint i=0; i<_ids.length; i++) {
approve(_to, _ids[i]);
}
}
}
function _mint(
address to,
Bond memory bond
) internal {
unchecked {
bondPaid[bond.id][bond.asset] = accRewardsPerShare[bond.asset][epoch[bond.asset]] * bond.shares / 1e18;
}
_mint(to, bond.id);
}
function _burn(
uint256 _id
) internal override {
delete _idToBond[_id];
super._burn(_id);
}
function _transfer(
address from,
address to,
uint256 _id
) internal override {
Bond memory bond = idToBond(_id);
require(epoch[bond.asset] == block.timestamp/DAY, "Bad epoch");
require(!bond.expired, "Expired!");
unchecked {
require(block.timestamp > bond.mintTime + 300, "Recent update");
userDebt[from][bond.asset] += bond.pending;
bondPaid[_id][bond.asset] += bond.pending;
}
super._transfer(from, to, _id);
}
function balanceIds(address _user) public view returns (uint[] memory) {
uint[] memory _ids = new uint[](balanceOf(_user));
unchecked {
for (uint i=0; i<_ids.length; i++) {
_ids[i] = tokenOfOwnerByIndex(_user, i);
}
}
return _ids;
}
function addAsset(address _asset) external onlyOwner {
require(assets.length == 0 || assets[assetsIndex[_asset]] != _asset, "Already added");
assetsIndex[_asset] = assets.length;
assets.push(_asset);
allowedAsset[_asset] = true;
epoch[_asset] = block.timestamp/DAY;
}
function setAllowedAsset(address _asset, bool _bool) external onlyOwner {
require(assets[assetsIndex[_asset]] == _asset, "Not added");
allowedAsset[_asset] = _bool;
}
function setBaseURI(string calldata _newBaseURI) external onlyOwner {
baseURI = _newBaseURI;
}
function setManager(
address _manager
) public onlyOwner() {
manager = _manager;
}
modifier onlyManager() {
require(msg.sender == manager, "!manager");
_;
}
event Distribution(address _tigAsset, uint256 _amount);
event Lock(address _tigAsset, uint256 _amount, uint256 _period, address _owner, uint256 _id);
event ExtendLock(uint256 _period, uint256 _amount, address _owner, uint256 _id);
event Release(address _tigAsset, uint256 _amount, address _owner, uint256 _id);
event ClaimFees(address _tigAsset, uint256 _amount, address _claimer, uint256 _id);
event ClaimDebt(address _tigAsset, uint256 _amount, address _owner);
}