-
Notifications
You must be signed in to change notification settings - Fork 11
/
NeoTokyoStaker.sol
1858 lines (1593 loc) · 58 KB
/
NeoTokyoStaker.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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../access/PermitControl.sol";
import "../interfaces/IByteContract.sol";
import "../interfaces/IGenericGetter.sol";
/**
Thrown during when attempting to operate on a non-existent Citizen (S1 or S2).
@param citizenId The ID of the caller's specified Citizen.
*/
error CitizenDoesNotExist (
uint256 citizenId
);
/**
Thrown when attempting to get a staker's position of an unknowable asset type.
@param assetType The caller's specified asset type.
*/
error UnknowablePosition (
uint256 assetType
);
/**
Thrown when an S1 Citizen with a component Vault attempts to stake while
attaching an optional non-component Vault.
@param componentVaultId The ID of the S1 Citizen's component Vault.
@param noncomponentVaultId The ID of the Vault the caller attempted to stake.
*/
error CitizenAlreadyHasVault (
uint256 componentVaultId,
uint256 noncomponentVaultId
);
/**
Thrown when an S1 Citizen attempts to wrongfully claim the Hand bonus.
@param citizenId The ID of the caller's specified S1 Citizen.
*/
error CitizenIsNotHand (
uint256 citizenId
);
/**
Thrown when a BYTES stake would exceed the cap of its corresponding Citizen.
@param attemptedAmount The amount that the user is attempting to stake to.
@param cap The staking cap of the Citizen.
*/
error AmountExceedsCap (
uint256 attemptedAmount,
uint256 cap
);
/**
Thrown when attempting to stake BYTES into an unowned Citizen.
@param citizenId The token ID of the Citizen involved in the attempted stake.
@param seasonId The season ID of the Citizen, whether S1 or S2.
*/
error CannotStakeIntoUnownedCitizen (
uint256 citizenId,
uint256 seasonId
);
/**
Thrown when attempting to stake BYTES into an invalid Citizen season.
@param seasonId The ID of the Citizen season to try staking BYTES into.
*/
error InvalidSeasonId (
uint256 seasonId
);
/**
Thrown when attempting to increase a stake in an asset without matching the
existing timelock of the asset.
*/
error MismatchedTimelock ();
/**
Thrown during staking or unstaking if an invalid AssetType is specified.
@param assetType The caller's specified asset type.
*/
error InvalidAssetType (
uint256 assetType
);
/**
Thrown during staking if attempting to stake into an unconfigured asset pool.
@param assetType The caller's specified asset type.
*/
error UnconfiguredPool (
uint256 assetType
);
/**
Thrown during staking if attempting to stake into an asset pool whose rewards
are not yet active.
@param assetType The caller's specified asset type.
*/
error InactivePool (
uint256 assetType
);
/**
Thrown during staking if an invalid timelock option is specified. Each
AssetType being staked may have independently-configured timelock options.
@param assetType The caller's specified asset type.
@param timelockId The caller's specified timelock ID against `assetType`.
*/
error InvalidTimelockOption (
uint256 assetType,
uint256 timelockId
);
/// Thrown if the caller of a function is not the BYTES contract.
error CallerIsNotBYTES ();
/**
Thrown when withdrawing an asset fails to clear a timelock.
@param endTime The time that the staked asset timelock ends.
*/
error TimelockNotCleared (
uint256 endTime
);
/**
Thrown when attempting to withdraw an unowned S1 Citizen.
@param citizenId The ID of the S1 Citizen attempted to be withdrawn.
*/
error CannotWithdrawUnownedS1 (
uint256 citizenId
);
/**
Thrown when attempting to withdraw an unowned S2 Citizen.
@param citizenId The ID of the S2 Citizen attempted to be withdrawn.
*/
error CannotWithdrawUnownedS2 (
uint256 citizenId
);
/**
Thrown if a caller tries to withdraw more LP tokens than they had staked.
@param attemptedWithdraw The amount of LP tokens that the caller attempted to
withdraw.
@param position The amount of LP tokens that the caller has actually staked.
*/
error NotEnoughLPTokens (
uint256 attemptedWithdraw,
uint256 position
);
/// Thrown if attempting to configure the LP token address post-lock.
error LockedConfigurationOfLP ();
/// Thrown when specifying invalid reward windows for a pool.
error RewardWindowTimesMustIncrease ();
/**
@custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM
@title A pool-based staking contract for the Neo Tokyo ecosystem.
@author Tim Clancy <@_Enoch>
@author Rostislav Khlebnikov <@catpic5buck>
This contract allows callers to stake their Neo Tokyo Citizens (both S1 and
S2) and BYTES for time-locked emission rewards. The staker operates on a
point-based, competitive system where stakers compete for a finite amount of
daily emissions. It allows permissioned managers to configure various
emission details for the Neo Tokyo ecosystem.
@custom:date February 14th, 2023.
*/
contract NeoTokyoStaker is PermitControl, ReentrancyGuard {
/// The `transferFrom` selector for ERC-20 and ERC-721 tokens.
bytes4 constant private _TRANSFER_FROM_SELECTOR = 0x23b872dd;
/// The `transfer` selector for ERC-20 tokens.
bytes4 constant private _TRANSFER_SELECTOR = 0xa9059cbb;
/// A constant multiplier to reduce overflow in staking calculations.
uint256 constant private _PRECISION = 1e12;
/// A constant divisor to calculate points and multipliers as basis points.
uint256 constant private _DIVISOR = 100;
/// The number of BYTES needed to get one point in BYTES staking calculations.
uint256 constant private _BYTES_PER_POINT = 200 * 1e18;
/// The identifier for the right to configure the LP token address.
bytes32 public constant CONFIGURE_LP = keccak256("CONFIGURE_LP");
/// The identifier for the right to configure timelock options.
bytes32 public constant CONFIGURE_TIMELOCKS = keccak256(
"CONFIGURE_TIMELOCKS"
);
/// The identifier for the right to configure Identity and Vault points.
bytes32 public constant CONFIGURE_CREDITS = keccak256("CONFIGURE_CREDITS");
/// The identifier for the right to configure emission rates and the DAO tax.
bytes32 public constant CONFIGURE_POOLS = keccak256("CONFIGURE_POOLS");
/// The identifier for the right to configure BYTES staking caps.
bytes32 public constant CONFIGURE_CAPS = keccak256("CONFIGURE_CAPS");
/// The address of the new BYTES 2.0 token contract.
address immutable public BYTES;
/// The address of the assembled Neo Tokyo S1 Citizen contract.
address immutable public S1_CITIZEN;
/// The address of the assembled Neo Tokyo S2 Citizen contract.
address immutable public S2_CITIZEN;
/// The address of the LP token contract.
address public LP;
/**
The address of the Neo Tokyo S1 Identity contract. This specific contract
address is stored to check an assembled S1 Citizen's specific component
identity in order to check for Hands of the Citadel.
*/
address immutable public IDENTITY;
/// The address of the Neo Tokyo S1 Vault contract.
address immutable public VAULT;
/**
The limit on the number of BYTES that may be staked per S1 Citizen
assembled with a component Vault or per Vault-less S1 Citizen staked
alongside a Vault.
*/
uint256 public VAULT_CAP;
/**
The limit on the number of BYTES that may be staked per S2 Citizen or S1
Citizen without Vault.
*/
uint256 public NO_VAULT_CAP;
/**
This enum tracks each type of asset that may be operated on with this
staker.
@param S1_CITIZEN A staked Neo Tokyo S1 Citizen.
@param S2_CITIZEN A staked Neo Tokyo S2 Citizen.
@param BYTES A set of staked BYTES ERC-20 token.
@param LP Staked BYTES-ETH LP ERC-20 token.
*/
enum AssetType {
S1_CITIZEN,
S2_CITIZEN,
BYTES,
LP
}
/**
This mapping contains the per-asset configuration of different timelock
periods with their associated multipliers. For each asset, the interior
mapping correlates a particular timelock option to a uint256 which encodes
the duration of the timelock in its upper 128 bits and the multiplier
offered by that timelock, as basis points, in its lower 128 bits.
*/
mapping ( AssetType => mapping ( uint256 => uint256 )) public timelockOptions;
/**
This struct defines a specific time window in reward emission history. For
a particular asset staking pool, it represents that beginning from
`startTime`, the pool had a per-second reward emission rate of `reward`.
@param startTime The time at which the daily reward activated.
@param reward The reward emission rate beginning at `startTime`.
*/
struct RewardWindow {
uint128 startTime;
uint128 reward;
}
/**
This struct is used to define both the configuration details for a
particular asset staking pool and the state of that pool as callers begin
interacting with it.
@param totalPoints The total number of points in the pool.
@param daoTax The percent, in basis points, of the reward emissions sent to
the DAO.
@param rewardCount A count of the number of reward windows in the
`rewardWindows` mapping, used for iterating.
@param rewardWindows A mapping of the historic amount of BYTES token
rewarded per-second across all stakers in this particular pool.
*/
struct PoolData {
uint256 totalPoints;
uint256 daoTax;
uint256 rewardCount;
mapping ( uint256 => RewardWindow ) rewardWindows;
}
/// Map an asset type to its corresponding pool data.
mapping ( AssetType => PoolData ) private _pools;
/// Track the last time a caller was granted their rewards for each asset.
mapping ( address => mapping ( AssetType => uint256 )) public lastRewardTime;
/**
This admin-configurable double-mapping allows us to deduce the Identity
"Credit Yield" string of a Neo Tokyo S1 Citizen given the Citizen's reward
rate and the reward rate of the Citizen's Vault.
*/
mapping ( uint256 => mapping ( string => string )) public identityCreditYield;
/// Assign a configurable multiplier to each S1 Citizen's Identity credit.
mapping ( string => uint256 ) public identityCreditPoints;
/// Assign a configurable multiplier to each S1 Citizen's Vault credit.
mapping ( string => uint256 ) public vaultCreditMultiplier;
/**
This struct records the state of each staked S1 Citizen.
@param stakedBytes The number of BYTES that have been staked into this S1
Citizen. Depending on the value of `hasVault`, this S1 Citizen will be
bound to either the `VAULT_CAP` limit or `NO_VAULT_CAP` limit on the
number of BYTES that may be staked.
@param timelockEndTime The time at which the forced, timelocked staking of
this S1 Citizen ends. After this time the S1 Citizen may be withdrawn
from the staker.
@param points The number of base points that this staked S1 Citizen is
worth, thus determining its share of emissions. An S1 Citizen's base
points are a function of the S1 Citizen's component Identity and any
associated Vault multiplier. The base points are also multiplied by the
timelock option chosen at the time of staking. The base points are
supplemented in points calculations by the value of `stakedBytes`.
@param stakedVaultId The optional ID of the Vault, if there is one, that
has been staked alongside this S1 Citizen. If `hasVault` is true, this
value may be non-zero to indicate a staked but non-component Vault. If
`hasVault` is true and this value is zero, that is indicative of an S1
Citizen with a component Vault.
@param hasVault A boolean indicating whether or not this S1 Citizen has an
associated Vault, whether that Vault is a component Vault assembled into
the S1 Citizen or one that has been staked alongside the S1 Citizen.
*/
struct StakedS1Citizen {
uint256 stakedBytes;
uint256 timelockEndTime;
uint256 points;
uint256 stakedVaultId;
bool hasVault;
}
/**
A double mapping from a caller address to a specific S1 Citizen token ID to
the staking status of each S1 Citizen. This records the unique per-user
staking status of each S1 citizen.
*/
mapping ( address => mapping( uint256 => StakedS1Citizen )) public stakedS1;
/**
This mapping correlates a caller address to a list of their
currently-staked S1 Citizen token IDs.
*/
mapping ( address => uint256[] ) private _stakerS1Position;
/**
This struct records the state of each staked S2 Citizen.
@param stakedBytes The number of BYTES that have been staked into this S2
Citizen.
@param timelockEndTime The time at which the forced, timelocked staking of
this S2 Citizen ends. After this time the S2 Citizen may be withdrawn
from the staker.
@param points The number of base points that this staked S2 Citizen is
worth, thus determining its share of emissions. An S2 Citizen's base
points are a function of the timelock option chosen at the time of
staking. The base points are supplemented in points calculations by the
value of `stakedBytes`.
*/
struct StakedS2Citizen {
uint256 stakedBytes;
uint256 timelockEndTime;
uint256 points;
}
/**
A double mapping from a caller address to a specific S2 Citizen token ID to
the staking status of each S2 Citizen. This records the unique per-user
staking status of each S2 citizen.
*/
mapping ( address => mapping( uint256 => StakedS2Citizen )) public stakedS2;
/**
This mapping correlates a caller address to a list of their
currently-staked S2 Citizen token IDs.
*/
mapping ( address => uint256[] ) private _stakerS2Position;
/**
This struct defines the LP token staking position of a particular staker.
@param amount The amount of LP tokens staked by the staker.
@param timelockEndTime The tiume at which the forced, timelocked staking of
these LP tokens ends. After this the LP tokens may be withdrawn.
@param points The number of points that this LP position accrues.
@param multiplier The multiplier portion of the timelock option recorded so
as to enforce later stakes to use the same point rate.
*/
struct LPPosition {
uint256 amount;
uint256 timelockEndTime;
uint256 points;
uint256 multiplier;
}
/**
This mapping correlates each caller address to details regarding the LP
token stake of that caller.
*/
mapping ( address => LPPosition ) public stakerLPPosition;
/**
This struct supplies the position output state of each staked S1 Citizen.
@param citizenId The token ID of this S1 Citizen.
@param stakedBytes The number of BYTES that have been staked into this S1
Citizen. Depending on the value of `hasVault`, this S1 Citizen will be
bound to either the `VAULT_CAP` limit or `NO_VAULT_CAP` limit on the
number of BYTES that may be staked.
@param timelockEndTime The time at which the forced, timelocked staking of
this S1 Citizen ends. After this time the S1 Citizen may be withdrawn
from the staker.
@param points The number of base points that this staked S1 Citizen is
worth, thus determining its share of emissions. An S1 Citizen's base
points are a function of the S1 Citizen's component Identity and any
associated Vault multiplier. The base points are also multiplied by the
timelock option chosen at the time of staking. The base points are
supplemented in points calculations by the value of `stakedBytes`.
@param stakedVaultId The optional ID of the Vault, if there is one, that
has been staked alongside this S1 Citizen. If `hasVault` is true, this
value may be non-zero to indicate a staked but non-component Vault. If
`hasVault` is true and this value is zero, that is indicative of an S1
Citizen with a component Vault.
@param hasVault A boolean indicating whether or not this S1 Citizen has an
associated Vault, whether that Vault is a component Vault assembled into
the S1 Citizen or one that has been staked alongside the S1 Citizen.
*/
struct StakedS1CitizenOutput {
uint256 citizenId;
uint256 stakedBytes;
uint256 timelockEndTime;
uint256 points;
uint256 stakedVaultId;
bool hasVault;
}
/**
This struct supplies the position output state of each staked S2 Citizen.
@param citizenId The token ID of this S1 Citizen.
@param stakedBytes The number of BYTES that have been staked into this S2
Citizen.
@param timelockEndTime The time at which the forced, timelocked staking of
this S2 Citizen ends. After this time the S2 Citizen may be withdrawn
from the staker.
@param points The number of base points that this staked S2 Citizen is
worth, thus determining its share of emissions. An S2 Citizen's base
points are a function of the timelock option chosen at the time of
staking. The base points are supplemented in points calculations by the
value of `stakedBytes`.
*/
struct StakedS2CitizenOutput {
uint256 citizenId;
uint256 stakedBytes;
uint256 timelockEndTime;
uint256 points;
}
/**
This struct records the state of all assets staked by a particular staker
address in this staker.
@param stakedS1Citizens An array containing information about each S1
Citizen staked by a particular staker address.
@param stakedS2Citizens An array containing information about each S2
Citizen staked by a particular staker address.
@param stakedLPPosition Details regarding the LP token stake of a particular
staker address.
*/
struct StakerPosition {
StakedS1CitizenOutput[] stakedS1Citizens;
StakedS2CitizenOutput[] stakedS2Citizens;
LPPosition stakedLPPosition;
}
/// Whether or not setting the LP token contract address is locked.
bool public lpLocked;
/**
This struct records an input to the staker's `configurePools` function.
@param assetType The asset type for the corresponding pool to set.
@param daoTax The percent, in basis points, of the reward emissions sent to
the DAO.
@param rewardWindows An array specifying the historic amount of BYTES token
rewarded per-second across all stakers in this particular pool.
*/
struct PoolConfigurationInput {
AssetType assetType;
uint256 daoTax;
RewardWindow[] rewardWindows;
}
/**
This event is emitted when an asset is successfully staked.
@param staker The address of the caller who staked an `asset`.
@param asset The address of the asset being staked.
@param timelockOption Data encoding the parameters surrounding the timelock
option used in staking the particular asset. Alternatively, this encodes
ctiizen information for BYTES staking.
@param amountOrTokenId The amount of `asset` staked or, for S1 and S2
Citizens, the ID of the specific token staked.
*/
event Stake (
address indexed staker,
address indexed asset,
uint256 timelockOption,
uint256 amountOrTokenId
);
/**
This event is emitted each time a recipient claims a reward.
@param recipient The recipient of the reward.
@param reward The amount of BYTES rewarded to the `recipient`.
@param tax The amount of BYTES minted as tax to the DAO.
*/
event Claim (
address indexed recipient,
uint256 reward,
uint256 tax
);
/**
This event is emitted when an asset is successfully withdrawn.
@param caller The address of the caller who withdrew an `asset`.
@param asset The address of the asset being withdrawn.
@param amountOrTokenId The amount of `asset` withdrawn or, for S1 and S2
Citizens, the ID of the specific token withdrawn.
*/
event Withdraw (
address indexed caller,
address indexed asset,
uint256 amountOrTokenId
);
/**
Construct a new instance of this Neo Tokyo staker configured with the given
immutable contract addresses.
@param _bytes The address of the BYTES 2.0 ERC-20 token contract.
@param _s1Citizen The address of the assembled Neo Tokyo S1 Citizen.
@param _s2Citizen The address of the assembled Neo Tokyo S2 Citizen.
@param _lpToken The address of the LP token.
@param _identity The address of the specific Neo Tokyo Identity sub-item.
@param _vault The address of the specific Neo Tokyo Vault sub-item.
@param _vaultCap The limit on the number of BYTES that may be staked per S1
Citizen assembled with a component Vault or staked alongside a Vault.
@param _noVaultCap The limit on the number of BYTES that may be staked per
S2 Citizen or S1 Citizen without Vault.
*/
constructor (
address _bytes,
address _s1Citizen,
address _s2Citizen,
address _lpToken,
address _identity,
address _vault,
uint256 _vaultCap,
uint256 _noVaultCap
) {
BYTES = _bytes;
S1_CITIZEN = _s1Citizen;
S2_CITIZEN = _s2Citizen;
LP = _lpToken;
IDENTITY = _identity;
VAULT = _vault;
VAULT_CAP = _vaultCap;
NO_VAULT_CAP = _noVaultCap;
}
/**
Neo Tokyo Identity items do not expose their "Credit Yield" trait values in
an easily-consumed fashion. This function works backwards to calculate the
underlying "Credit Yield" trait value of the component Identity item of the
Neo Tokyo S1 Citizen with the token ID of `_citizenId` given the reward
rate of the S1 Citizen as a whole and the credit multiplier of any
component Vault.
@param _citizenId The token ID of the Neo Tokyo S1 Citizen to retrieve an
Identity "Credit Yield" trait value for.
@param _vaultId The token ID of the Neo Tokyo S1 Citizen's component Vault,
if there is one. This parameter is separated to optimized for callers who
have already predetermined the token ID of the Vault.
@return The "Credit Yield" trait value of the component Identity item of
the S1 Citizen with the token ID of `_citizenId`.
*/
function getCreditYield (
uint256 _citizenId,
uint256 _vaultId
) public view returns (string memory) {
// Retrieve the total reward rate of this S1 Citizen.
IGenericGetter citizen = IGenericGetter(S1_CITIZEN);
uint256 rewardRate = citizen.getRewardRateOfTokenId(_citizenId);
if (rewardRate == 0) {
revert CitizenDoesNotExist(_citizenId);
}
// Retrieve the credit rate multiplier of any associated Vault.
IGenericGetter vault = IGenericGetter(VAULT);
string memory vaultMultiplier = (_vaultId != 0)
? vault.getCreditMultiplier(_vaultId)
: "";
// Deduce the original Identity credit yield.
return identityCreditYield[rewardRate][vaultMultiplier];
}
/**
The multipliers to S1 Citizen points contributed by their component Vaults
may be independently configured by permitted administrators of this staking
contract. This helper function returns any of the multipliers that may have
been configured.
@param _vaultId The token ID of a Neo Tokyo S1 Vault to retrieve the
configued multiplier for.
@return The configured point multiplier for the Vault with token ID of
`_vaultId`.
*/
function getConfiguredVaultMultiplier (
uint256 _vaultId
) public view returns (uint256) {
// Retrieve the credit rate multiplier of the Vault.
IGenericGetter vault = IGenericGetter(VAULT);
string memory vaultMultiplier = (_vaultId != 0)
? vault.getCreditMultiplier(_vaultId)
: "";
// Deduce the configured Vault multiplier.
return vaultCreditMultiplier[vaultMultiplier];
}
/**
Return the list of `_staker`'s token IDs for the specified `_assetType` if
that type is the Neo Tokyo S1 Citizen or S2 Citizen. In order to determine
the staker's position in the LP asset type, the public `stakerLPPosition`
mapping should be used. It is not valid to directly determine the position
in BYTES of a particular staker; to retrieve that kind of cumulative data
the full output `getStakerPositions` function should be used.
@param _staker The address of the staker to check for staked Citizen
holdings.
@param _assetType The asset type to check for staked holdings. This must be
the S1 Citizen or S2 Citizen type.
@return The list of token IDs of a particular Citizen type that have been
staked by `_staker`.
*/
function getStakerPosition (
address _staker,
AssetType _assetType
) external view returns (uint256[] memory) {
if (_assetType == AssetType.S1_CITIZEN) {
return _stakerS1Position[_staker];
} else if (_assetType == AssetType.S2_CITIZEN) {
return _stakerS2Position[_staker];
} else {
revert UnknowablePosition(uint256(_assetType));
}
}
/**
Retrieve the entire position of the specified `_staker` across all asset
types in this staker.
@param _staker The address of the staker to check for assets.
@return The position of the `_staker` across all asset types.
*/
function getStakerPositions (
address _staker
) external view returns (StakerPosition memory) {
// Compile the S1 Citizen details.
StakedS1CitizenOutput[] memory stakedS1Details =
new StakedS1CitizenOutput[](_stakerS1Position[_staker].length);
for (uint256 i; i < _stakerS1Position[_staker].length; ) {
uint256 citizenId = _stakerS1Position[_staker][i];
StakedS1Citizen memory citizenDetails = stakedS1[_staker][citizenId];
stakedS1Details[i] = StakedS1CitizenOutput({
citizenId: citizenId,
stakedBytes: citizenDetails.stakedBytes,
timelockEndTime: citizenDetails.timelockEndTime,
points: citizenDetails.points,
hasVault: citizenDetails.hasVault,
stakedVaultId: citizenDetails.stakedVaultId
});
unchecked { i++; }
}
// Compile the S2 Citizen details.
StakedS2CitizenOutput[] memory stakedS2Details =
new StakedS2CitizenOutput[](_stakerS2Position[_staker].length);
for (uint256 i; i < _stakerS2Position[_staker].length; ) {
uint256 citizenId = _stakerS2Position[_staker][i];
StakedS2Citizen memory citizenDetails = stakedS2[_staker][citizenId];
stakedS2Details[i] = StakedS2CitizenOutput({
citizenId: citizenId,
stakedBytes: citizenDetails.stakedBytes,
timelockEndTime: citizenDetails.timelockEndTime,
points: citizenDetails.points
});
unchecked { i++; }
}
// Return the final output position struct.
return StakerPosition({
stakedS1Citizens: stakedS1Details,
stakedS2Citizens: stakedS2Details,
stakedLPPosition: stakerLPPosition[_staker]
});
}
/**
A private helper function for performing the low-level call to
`transferFrom` on either a specific ERC-721 token or some amount of ERC-20
tokens.
@param _asset The address of the asset to perform the transfer call on.
@param _from The address to attempt to transfer the asset from.
@param _to The address to attempt to transfer the asset to.
@param _idOrAmount This parameter encodes either an ERC-721 token ID or an
amount of ERC-20 tokens to attempt to transfer, depending on what
interface is implemented by `_asset`.
*/
function _assetTransferFrom (
address _asset,
address _from,
address _to,
uint256 _idOrAmount
) private {
(bool success, bytes memory data) =
_asset.call(
abi.encodeWithSelector(
_TRANSFER_FROM_SELECTOR,
_from,
_to,
_idOrAmount
)
);
// Revert if the low-level call fails.
if (!success) {
revert(string(data));
}
}
/**
A private helper function for performing the low-level call to `transfer`
on some amount of ERC-20 tokens.
@param _asset The address of the asset to perform the transfer call on.
@param _to The address to attempt to transfer the asset to.
@param _amount The amount of ERC-20 tokens to attempt to transfer.
*/
function _assetTransfer (
address _asset,
address _to,
uint256 _amount
) private {
(bool success, bytes memory data) =
_asset.call(
abi.encodeWithSelector(
_TRANSFER_SELECTOR,
_to,
_amount
)
);
// Revert if the low-level call fails.
if (!success) {
revert(string(data));
}
}
/**
A private helper for checking equality between two strings.
@param _a The first string to compare.
@param _b The second string to compare.
@return Whether or not `_a` and `_b` are equal.
*/
function _stringEquals (
string memory _a,
string memory _b
) private pure returns (bool) {
bytes memory a = bytes(_a);
bytes memory b = bytes(_b);
// Check equivalence of the two strings by comparing their contents.
bool equal = true;
assembly {
let length := mload(a)
switch eq(length, mload(b))
// Proceed to compare string contents if lengths are equal.
case 1 {
let cb := 1
// Iterate through the strings and compare contents.
let mc := add(a, 0x20)
let end := add(mc, length)
for {
let cc := add(b, 0x20)
} eq(add(lt(mc, end), cb), 2) {
mc := add(mc, 0x20)
cc := add(cc, 0x20)
} {
// If any of these checks fails then arrays are not equal.
if iszero(eq(mload(mc), mload(cc))) {
equal := 0
cb := 0
}
}
}
// By default the array length is not equal so the strings are not equal.
default {
equal := 0
}
}
return equal;
}
/**
A private helper function for managing the staking of a particular S1
Citizen. S1 Citizens may optionally be staked at the same time as a Vault,
if they do not already contain a Vault.
@param _timelock The selected timelock option for the asset being staked.
This encodes the timelock duration and multiplier.
*/
function _stakeS1Citizen (
uint256 _timelock
) private {
uint256 citizenId;
uint256 vaultId;
uint256 handClaimant;
/*
Extract the S1 Citizen ID, optional Vault token ID, and optional Hand
claimant ID from calldata.
*/
assembly {
citizenId := calldataload(0x44)
vaultId := calldataload(0x64)
handClaimant := calldataload(0x84)
}
/*
Attempt to transfer the S1 Citizen to be held in escrow by this staking
contract. This transfer will fail if the caller is not the holder of the
Citizen. This prevents double staking.
*/
_assetTransferFrom(S1_CITIZEN, msg.sender, address(this), citizenId);
// Retrieve storage for tracking the staking state of this S1 Citizen.
StakedS1Citizen storage citizenStatus = stakedS1[msg.sender][citizenId];
// Attach a getter to the S1 Citizen and check for a component Vault.
IGenericGetter citizen = IGenericGetter(S1_CITIZEN);
uint256 citizenVaultId = citizen.getVaultIdOfTokenId(citizenId);
/*
A new Vault to stake may only be provided if the S1 Citizen being staked
does not already have a component Vault.
*/
if (citizenVaultId != 0 && vaultId != 0) {
revert CitizenAlreadyHasVault(citizenVaultId, vaultId);
/*
If no optional vault is provided, and the S1 Citizen being staked already
has an existing Vault, override the provided `vaultId`.
*/
} else if (citizenVaultId != 0 && vaultId == 0) {
citizenStatus.hasVault = true;
vaultId = citizenVaultId;
/*
Otherwise, if the S1 Citizen has no component Vault, the newly-provided
Vault is staked and the S1 Citizen is recorded as carrying an optional,
separately-attached vault.
*/
} else if (citizenVaultId == 0 && vaultId != 0) {
_assetTransferFrom(VAULT, msg.sender, address(this), vaultId);
citizenStatus.hasVault = true;
citizenStatus.stakedVaultId = vaultId;
}
/*
If the S1 Citizen contains no component Vault and is not staked alongside
an optional Vault (`citizenVaultId` == 0 && `vaultId` == 0), we need not
do anything to change the initial state of a staked S1 Citizen's Vault.
*/
// Determine the base worth in points of the S1 Citizen's Identity.
string memory citizenCreditYield = getCreditYield(
citizenId,
citizenVaultId
);
uint256 identityPoints = identityCreditPoints[citizenCreditYield];
// Hands of the Citadel are always given the same multiplier as '?' Vaults.
uint256 vaultMultiplier = 100;
if (handClaimant == 1) {
uint256 identityId = citizen.getIdentityIdOfTokenId(citizenId);
string memory class = IGenericGetter(IDENTITY).getClass(identityId);
if (_stringEquals(class, "Hand of Citadel")) {
vaultMultiplier = vaultCreditMultiplier["?"];
} else {
revert CitizenIsNotHand(citizenId);
}
// Otherwise use the configured Vault multiplier, if any.
} else if (vaultId != 0) {
vaultMultiplier = getConfiguredVaultMultiplier(vaultId);
}
// Decode the timelock option's duration and multiplier.
uint256 timelockDuration = _timelock >> 128;
uint256 timelockMultiplier = _timelock & type(uint128).max;
// Update caller staking information and asset data.
PoolData storage pool = _pools[AssetType.S1_CITIZEN];
unchecked {
citizenStatus.points =
identityPoints * vaultMultiplier * timelockMultiplier /
_DIVISOR / _DIVISOR;
citizenStatus.timelockEndTime = block.timestamp + timelockDuration;
// Record the caller's staked S1 Citizen.
_stakerS1Position[msg.sender].push(citizenId);
// Update the pool point weights for rewards
pool.totalPoints += citizenStatus.points;
}
// Emit an event recording this S1 Citizen staking.
emit Stake(
msg.sender,
S1_CITIZEN,
_timelock,
citizenId
);
}
/**
A private function for managing the staking of a particular S2 Citizen.
@param _timelock The selected timelock option for the asset being staked.
This encodes the timelock duration and multiplier.
*/
function _stakeS2Citizen (
uint256 _timelock
) private {
uint256 citizenId;
// Extract the S2 Citizen ID from the calldata.