-
Notifications
You must be signed in to change notification settings - Fork 10
/
GardenOfForkingPaths.sol
1170 lines (1054 loc) · 42.9 KB
/
GardenOfForkingPaths.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: Apache-2.0
/**
* Authors: Moonstream Engineering (engineering@moonstream.to)
* GitHub: https://github.com/bugout-dev/engine
*/
pragma solidity ^0.8.0;
import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import "@openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {LibDiamondMoonstream as LibDiamond} from "../../diamond/libraries/LibDiamondMoonstream.sol";
import "../../diamond/security/DiamondReentrancyGuard.sol";
import {InventoryFacet} from "../../inventory/InventoryFacet.sol";
import {TerminusFacet} from "../../terminus/TerminusFacet.sol";
import {TerminusPermissions} from "../../terminus/TerminusPermissions.sol";
uint256 constant ERC20_TYPE = 20;
uint256 constant ERC721_TYPE = 721;
uint256 constant ERC1155_TYPE = 1155;
uint256 constant TERMINUS_MINTABLE_TYPE = 1;
struct Session {
address playerTokenAddress;
address paymentTokenAddress;
uint256 paymentAmount;
bool isActive; // active -> stake if ok, cannot unstake
bool isChoosingActive; // if active -> players can choose path in current stage
string uri;
uint256[] stages;
// In forgiving sessions, making the wrong path choice at the previous stage doesn't prevent a
// player from choosing a path in the next stage. It *does* prevent them from collecting the reward
// for the next stage, though.
bool isForgiving;
}
/**
Reward represents the reward an NFT owner can collect by making a choice with their NFT on the
corresponding stage in a given Garden of Forking Paths session.
The reward must be a Terminus token and the Garden of Forking Paths contract must have minting privileges
on the token pool.
*/
struct Reward {
uint256 rewardType; // 1, 1155, 20, 721 - 1 means mint Terminus, 1155 means transfer 1155
address rewardAddress;
uint256 rewardTokenID;
uint256 rewardAmount;
address inventoryAddress; // if 0, reward goes to player/staker, else to NFT
uint256 inventorySlot;
}
struct Predicate {
address predicateAddress;
bytes4 functionSelector;
// initialArguments is intended to be ABI encoded partial arguments to the predicate function.
bytes initialArguments;
}
struct PathDetails {
uint256 sessionId;
uint256 stageNumber;
uint256 pathNumber;
}
library LibGOFP {
bytes32 constant STORAGE_POSITION =
keccak256("moonstreamdao.eth.storage.mechanics.GardenOfForkingPaths");
/**
All implicit arrays (implemented with maps) are 1-indexed. This applies to:
- sessions
- stages
- paths
This helps us avoid any confusion that stems from 0 being the default value for uint256.
Applying this condition uniformly to all mappings avoids confusion from having to remember which
implicit arrays are 0-indexed and which are 1-indexed.
*/
struct GOFPStorage {
address AdminTerminusAddress;
uint256 AdminTerminusPoolID;
uint256 numSessions;
mapping(uint256 => Session) sessionById;
// session => stage => stageReward
mapping(uint256 => mapping(uint256 => Reward)) sessionStageReward;
// session => stage => correct path for that stage
mapping(uint256 => mapping(uint256 => uint256)) sessionStagePath;
// nftAddress => tokenId => sessionId
mapping(address => mapping(uint256 => uint256)) stakedTokenSession;
// nftAddress => tokenId => owner
mapping(address => mapping(uint256 => address)) stakedTokenOwner;
// session => owner => numTokensStaked
mapping(uint256 => mapping(address => uint256)) numTokensStakedByOwnerInSession;
// sessionId => tokenId => index in tokensStakedByOwnerInSession
mapping(uint256 => mapping(uint256 => uint256)) stakedTokenIndex;
// session => owner => index => tokenId
// The index refers to the tokens that the given owner has staked into the given sessions.
// The index starts from 1.
mapping(uint256 => mapping(address => mapping(uint256 => uint256))) tokensStakedByOwnerInSession;
// session => tokenId => stage => chosenPath
// This mapping tracks the path chosen by each eligible NFT in a session at each stage
mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) pathChoices;
// session => tokenId => was token ever staked into session?
// This guards against a token being staked into a session multiple times.
mapping(uint256 => mapping(uint256 => bool)) sessionTokenStakeGuard;
// GOFP v0.2: session => stage => path => reward
mapping(uint256 => mapping(uint256 => mapping(uint256 => Reward))) sessionPathReward;
// Predicate to check prior to staking into session
mapping(uint256 => Predicate) sessionStakingPredicate;
// Predicate to check prior to choosing path
mapping(uint256 => mapping(uint256 => mapping(uint256 => Predicate))) pathChoicePredicate;
}
function gofpStorage() internal pure returns (GOFPStorage storage gs) {
bytes32 position = STORAGE_POSITION;
assembly {
gs.slot := position
}
}
}
/**
The GOFPFacet is a smart contract that can either be used standalone or as part of an EIP2535 Diamond
proxy contract.
It implements the Garden of Forking Paths, a multiplayer choose your own adventure game mechanic.
Garden of Forking Paths is run in sessions. Each session consists of a given number of stages. Each
stage consists of a given number of paths.
Everything on the Garden of Forking Paths is 1-indexed.
There are two kinds of accounts that can interact with the Garden of Forking Paths:
1. Game Masters
2. Players
Game Masters are accounts which hold an admin badge as defined by LibGOFP.AdminTerminusAddress and
LibGOFP.AdminTerminusPoolID. The badge is expected to be a Terminus badge (non-transferable token).
Game Masters can:
- [x] Create sessions
- [x] Mark sessions as active or inactive
- [x] Mark sessions as active or inactive for the purposes of NFTs choosing a path in a the current stage
- [x] Register the correct path for the current stage
- [x] Update the metadata for a session
- [x] Set a reward (Terminus token mint) for NFT holders who make a choice with an NFT in each stage
Players can:
- [x] Stake their NFTs into a sesssion if the correct first stage path has not been chosen
- [x] Pay to stake their NFTs
- [x] Unstake their NFTs from a session at any time
- [x] Have one of their NFTs choose a path in the current stage PROVIDED THAT the current stage is the first
stage OR that the NFT chose the correct path in the previous stage
- [x] Collect their reward (Terminus token mint) for making a choice with an NFT in the current stage of a session
Anybody can:
- [x] View details of a session
- [x] View the correct path for a given stage
- [x] View how many tokens a given owner has staked into a given session
- [x] View the token ID of the <n>th token that a given owner has staked into a given session for any valid
value of n
*/
contract GOFPFacet is
ERC721Holder,
ERC1155Holder,
TerminusPermissions,
DiamondReentrancyGuard
{
modifier onlyGameMaster() {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
_holdsPoolToken(gs.AdminTerminusAddress, gs.AdminTerminusPoolID, 1),
"GOFPFacet.onlyGameMaster: The address is not an authorized game master"
);
_;
}
event SessionCreated(
uint256 sessionId,
address indexed playerTokenAddress,
address indexed paymentTokenAddress,
uint256 paymentAmount,
string uri,
bool active,
bool isForgiving
);
event SessionActivated(uint256 indexed sessionId, bool isActive);
event SessionChoosingActivated(
uint256 indexed sessionId,
bool isChoosingActive
);
event StageRewardChanged(
uint256 indexed sessionId,
uint256 indexed stage,
uint256 rewardType,
address rewardAddress,
uint256 rewardTokenID,
uint256 rewardAmount,
address inventoryAddress,
uint256 inventorySlot
);
event PathRewardChanged(
uint256 indexed sessionId,
uint256 indexed stage,
uint256 indexed path,
uint256 rewardType,
address rewardAddress,
uint256 rewardTokenID,
uint256 rewardAmount,
address inventoryAddress,
uint256 inventorySlot
);
event SessionUriChanged(uint256 indexed sessionId, string uri);
event PathRegistered(
uint256 indexed sessionId,
uint256 stage,
uint256 path
);
event PathChosen(
uint256 indexed sessionId,
uint256 indexed tokenId,
uint256 indexed stage,
uint256 path
);
event StakingPredicateSet(
uint256 indexed sessionId,
address predicateAddress,
bytes4 functionSelector,
bytes initialArguments
);
event PathChoicePredicateSet(
uint256 indexed sessionId,
uint256 indexed stage,
uint256 indexed path,
address predicateAddress,
bytes4 functionSelector,
bytes initialArguments
);
event InventoryEquipError(string error);
function init(
address adminTerminusAddress,
uint256 adminTerminusPoolID
) external {
LibDiamond.enforceIsContractOwner();
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
gs.AdminTerminusAddress = adminTerminusAddress;
gs.AdminTerminusPoolID = adminTerminusPoolID;
}
function gofpVersion() public pure returns (string memory, string memory) {
return ("Moonstream Garden of Forking Paths", "0.2.1");
}
function getSession(
uint256 sessionId
) external view returns (Session memory) {
return LibGOFP.gofpStorage().sessionById[sessionId];
}
function adminTerminusInfo() external view returns (address, uint256) {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
return (gs.AdminTerminusAddress, gs.AdminTerminusPoolID);
}
function numSessions() external view returns (uint256) {
return LibGOFP.gofpStorage().numSessions;
}
/**
Creates a Garden of Forking Paths session. The session is configured with:
- playerTokenAddress - this is the address of ERC721 tokens that can participate in the session
- paymentTokenAddress - this is the address of the ERC20 token that each NFT must pay to enter the session
- paymentAmount - this is the amount of the payment token that each NFT must pay to enter the session
- isActive - this determines if the session is active as soon as it is created or not
- isChoosingActive - this determines if NFTs can choose a path in the current stage or not, and is true
by default when the session is created
- uri - metadata uri describing the session
- stages - an array describing the number of path choices at each stage of the session
*/
function createSession(
address playerTokenAddress,
address paymentTokenAddress,
uint256 paymentAmount,
bool isActive,
string memory uri,
uint256[] memory stages,
bool isForgiving
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
gs.numSessions++;
require(
gs.sessionById[gs.numSessions].playerTokenAddress == address(0),
"GOFPFacet.createSession: Session already registered"
);
require(
playerTokenAddress != address(0),
"GOFPFacet.createSession: playerTokenAddress can't be zero address"
);
require(
paymentTokenAddress != address(0) || paymentAmount == 0,
"GOFPFacet.createSession: If paymentTokenAddress is the 0 address, paymentAmount should also be 0"
);
gs.sessionById[gs.numSessions] = Session({
playerTokenAddress: playerTokenAddress,
paymentTokenAddress: paymentTokenAddress,
paymentAmount: paymentAmount,
isActive: isActive,
isChoosingActive: true,
uri: uri,
stages: stages,
isForgiving: isForgiving
});
emit SessionCreated(
gs.numSessions,
playerTokenAddress,
paymentTokenAddress,
paymentAmount,
uri,
isActive,
isForgiving
);
emit SessionActivated(gs.numSessions, isActive);
emit SessionChoosingActivated(gs.numSessions, true);
emit SessionUriChanged(gs.numSessions, uri);
}
function getStageReward(
uint256 sessionId,
uint256 stage
) external view returns (Reward memory) {
return LibGOFP.gofpStorage().sessionStageReward[sessionId][stage];
}
function setStageRewards(
uint256 sessionId,
uint256[] calldata stages,
Reward[] calldata rewards
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
stages.length == rewards.length,
"GOFPFacet.setStageRewards: rewards must have same length as stages"
);
Session storage session = gs.sessionById[sessionId];
require(
!session.isActive,
"GOFPFacet.setStageRewards: Cannot set stage rewards on active session"
);
for (uint256 i = 0; i < stages.length; i++) {
require(
(1 <= stages[i]) && (stages[i] <= session.stages.length),
"GOFPFacet.setStageRewards: Invalid stage"
);
gs.sessionStageReward[sessionId][stages[i]] = Reward({
rewardType: rewards[i].rewardType,
rewardAddress: rewards[i].rewardAddress,
rewardTokenID: rewards[i].rewardTokenID,
rewardAmount: rewards[i].rewardAmount,
inventoryAddress: rewards[i].inventoryAddress,
inventorySlot: rewards[i].inventorySlot
});
emit StageRewardChanged(
sessionId,
stages[i],
rewards[i].rewardType,
rewards[i].rewardAddress,
rewards[i].rewardTokenID,
rewards[i].rewardAmount,
rewards[i].inventoryAddress,
rewards[i].inventorySlot
);
}
}
function getPathReward(
uint256 sessionId,
uint256 stage,
uint256 path
) external view returns (Reward memory) {
return LibGOFP.gofpStorage().sessionPathReward[sessionId][stage][path];
}
function setPathRewards(
uint256 sessionId,
uint256[] memory stages,
uint256[] memory paths,
Reward[] calldata rewards
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
stages.length == paths.length,
"GOFPFacet.setPathRewards: paths must have same length as stages"
);
require(
stages.length == rewards.length,
"GOFPFacet.setPathRewards: rewards must have same length as stages"
);
Session storage session = gs.sessionById[sessionId];
require(
!session.isActive,
"GOFPFacet.setPathRewards: Cannot set path rewards on active session"
);
for (uint256 i = 0; i < stages.length; i++) {
require(
(1 <= stages[i]) && (stages[i] <= session.stages.length),
"GOFPFacet.setPathRewards: Invalid stage"
);
require(
(1 <= paths[i]) && (paths[i] <= session.stages[stages[i] - 1]),
"GOFPFacet.setPathRewards: Invalid path"
);
gs.sessionPathReward[sessionId][stages[i]][paths[i]] = Reward({
rewardType: rewards[i].rewardType,
rewardAddress: rewards[i].rewardAddress,
rewardTokenID: rewards[i].rewardTokenID,
rewardAmount: rewards[i].rewardAmount,
inventoryAddress: rewards[i].inventoryAddress,
inventorySlot: rewards[i].inventorySlot
});
emit PathRewardChanged(
sessionId,
stages[i],
paths[i],
rewards[i].rewardType,
rewards[i].rewardAddress,
rewards[i].rewardTokenID,
rewards[i].rewardAmount,
rewards[i].inventoryAddress,
rewards[i].inventorySlot
);
}
}
function setSessionActive(
uint256 sessionId,
bool isActive
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.setSessionActive: Invalid session ID"
);
gs.sessionById[sessionId].isActive = isActive;
emit SessionActivated(sessionId, isActive);
}
function getCorrectPathForStage(
uint256 sessionId,
uint256 stage
) external view returns (uint256) {
require(
stage > 0,
"GOFPFacet.getCorrectPathForStage: Stages are 1-indexed, 0 is not a valid stage"
);
return LibGOFP.gofpStorage().sessionStagePath[sessionId][stage];
}
function setCorrectPathForStage(
uint256 sessionId,
uint256 stage,
uint256 path,
bool setIsChoosingActive
) external onlyGameMaster {
require(
stage > 0,
"GOFPFacet.setCorrectPathForStage: Stages are 1-indexed, 0 is not a valid stage"
);
uint256 stageIndex = stage - 1;
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.setCorrectPathForStage: Invalid session"
);
require(
stageIndex < gs.sessionById[sessionId].stages.length,
"GOFPFacet.setCorrectPathForStage: Invalid stage"
);
require(
!gs.sessionById[sessionId].isChoosingActive,
"GOFPFacet.setCorrectPathForStage: Deactivate isChoosingActive before setting the correct path"
);
// Paths are 1-indexed to avoid possible confusion involving default value of 0
require(
path >= 1 && path <= gs.sessionById[sessionId].stages[stageIndex],
"GOFPFacet.setCorrectPathForStage: Invalid path"
);
// We use the default value of 0 as a guard to check that path has not already been set for that
// stage. No changes allowed for a given stage after the path was already chosen.
require(
gs.sessionStagePath[sessionId][stage] == 0,
"GOFPFacet.setCorrectPathForStage: Path has already been chosen for that stage"
);
// You cannot set the path for a stage if the path for its previous stage has not been previously
// set.
// We use the stageIndex to access the path because stageIndex = stage - 1. This is just a
// convenience. It would be more correct to access the "stage - 1" key in the mapping.
require(
stage <= 1 || gs.sessionStagePath[sessionId][stageIndex] != 0,
"GOFPFacet.setCorrectPathForStage: Path not set for previous stage"
);
gs.sessionStagePath[sessionId][stage] = path;
gs.sessionById[sessionId].isChoosingActive = setIsChoosingActive;
emit PathRegistered(sessionId, stage, path);
emit SessionChoosingActivated(sessionId, setIsChoosingActive);
}
function setSessionChoosingActive(
uint256 sessionId,
bool isChoosingActive
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.setSessionChoosingActive: Invalid session ID"
);
gs.sessionById[sessionId].isChoosingActive = isChoosingActive;
emit SessionChoosingActivated(sessionId, isChoosingActive);
}
function setSessionUri(
uint256 sessionId,
string memory uri
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.setSessionChoosingActive: Invalid session ID"
);
gs.sessionById[sessionId].uri = uri;
emit SessionUriChanged(sessionId, uri);
}
/**
For a given NFT, specified by the `nftAddress` and `tokenId`, this view function returns:
1. The sessionId of the session into which the NFT is staked
2. The address of the staker
If the token is not currently staked in the Garden of Forking Paths contract, this method returns
0 for the sessionId and the 0 address as the staker.
*/
function getStakedTokenInfo(
address nftAddress,
uint256 tokenId
) external view returns (uint256, address) {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
return (
gs.stakedTokenSession[nftAddress][tokenId],
gs.stakedTokenOwner[nftAddress][tokenId]
);
}
function getSessionTokenStakeGuard(
uint256 sessionId,
uint256 tokenId
) external view returns (bool) {
return LibGOFP.gofpStorage().sessionTokenStakeGuard[sessionId][tokenId];
}
function numTokensStakedIntoSession(
uint256 sessionId,
address staker
) external view returns (uint256) {
return
LibGOFP.gofpStorage().numTokensStakedByOwnerInSession[sessionId][
staker
];
}
function tokenOfStakerInSessionByIndex(
uint256 sessionId,
address staker,
uint256 index
) external view returns (uint256) {
return
LibGOFP.gofpStorage().tokensStakedByOwnerInSession[sessionId][
staker
][index];
}
/**
Returns the path chosen by the given tokenId in the given session and stage.
Recall: sessions and stages are 1-indexed.
*/
function getPathChoice(
uint256 sessionId,
uint256 tokenId,
uint256 stage
) external view returns (uint256) {
return LibGOFP.gofpStorage().pathChoices[sessionId][tokenId][stage];
}
function _addTokenToEnumeration(
uint256 sessionId,
address owner,
address nftAddress,
uint256 tokenId
) internal {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
gs.stakedTokenSession[nftAddress][tokenId] == 0,
"GOFPFacet._addTokenToEnumeration: Token is already associated with a session on this contract"
);
require(
gs.stakedTokenOwner[nftAddress][tokenId] == address(0),
"GOFPFacet._addTokenToEnumeration: Token is already associated with an owner on this contract"
);
require(
gs.stakedTokenIndex[sessionId][tokenId] == 0,
"GOFPFacet._addTokenToEnumeration: Token was already added to enumeration"
);
gs.stakedTokenSession[nftAddress][tokenId] = sessionId;
gs.stakedTokenOwner[nftAddress][tokenId] = owner;
uint256 currStaked = gs.numTokensStakedByOwnerInSession[sessionId][
owner
];
gs.tokensStakedByOwnerInSession[sessionId][owner][
currStaked + 1
] = tokenId;
gs.stakedTokenIndex[sessionId][tokenId] = currStaked + 1;
gs.numTokensStakedByOwnerInSession[sessionId][owner]++;
}
function _removeTokenFromEnumeration(
uint256 sessionId,
address owner,
address nftAddress,
uint256 tokenId
) internal {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
gs.stakedTokenSession[nftAddress][tokenId] == sessionId,
"GOFPFacet._removeTokenFromEnumeration: Token is not associated with the given session"
);
require(
gs.stakedTokenOwner[nftAddress][tokenId] == owner,
"GOFPFacet._removeTokenFromEnumeration: Token is not associated with the given owner"
);
require(
gs.stakedTokenIndex[sessionId][tokenId] != 0,
"GOFPFacet._removeTokenFromEnumeration: Token wasn't added to enumeration"
);
delete gs.stakedTokenSession[nftAddress][tokenId];
delete gs.stakedTokenOwner[nftAddress][tokenId];
uint256 currStaked = gs.numTokensStakedByOwnerInSession[sessionId][
owner
];
uint256 currIndex = gs.stakedTokenIndex[sessionId][tokenId];
uint256 lastToken = gs.tokensStakedByOwnerInSession[sessionId][owner][
currStaked
];
require(
currIndex <= currStaked &&
gs.tokensStakedByOwnerInSession[sessionId][owner][currIndex] ==
tokenId,
"GOFPFacet._removeTokenFromEnumeration: Token wasn't staked by the given owner"
);
//swapping last element with element at given index
gs.tokensStakedByOwnerInSession[sessionId][owner][
currIndex
] = lastToken;
//updating last token's index
gs.stakedTokenIndex[sessionId][lastToken] = currIndex;
//deleting old lastToken
// TODO(zomglings): Test stake -> unstake -> restake
delete gs.stakedTokenIndex[sessionId][tokenId];
delete gs.tokensStakedByOwnerInSession[sessionId][owner][currStaked];
//updating staked count
gs.numTokensStakedByOwnerInSession[sessionId][owner]--;
}
function getSessionStakingPredicate(
uint256 sessionId
) external view returns (Predicate memory) {
return LibGOFP.gofpStorage().sessionStakingPredicate[sessionId];
}
function setSessionStakingPredicate(
uint256 sessionId,
bytes4 functionSelector,
address predicateAddress,
bytes calldata initialArguments
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
gs.sessionStakingPredicate[sessionId] = Predicate({
predicateAddress: predicateAddress,
functionSelector: functionSelector,
initialArguments: initialArguments
});
emit StakingPredicateSet(
sessionId,
predicateAddress,
functionSelector,
initialArguments
);
}
function getPathChoicePredicate(
PathDetails calldata path
) external view returns (Predicate memory) {
return
LibGOFP.gofpStorage().pathChoicePredicate[path.sessionId][
path.stageNumber
][path.pathNumber];
}
function setPathChoicePredicate(
PathDetails calldata path,
bytes4 functionSelector,
address predicateAddress,
bytes calldata initialArguments
) external onlyGameMaster {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
gs.pathChoicePredicate[path.sessionId][path.stageNumber][
path.pathNumber
] = Predicate({
predicateAddress: predicateAddress,
functionSelector: functionSelector,
initialArguments: initialArguments
});
emit PathChoicePredicateSet(
path.sessionId,
path.stageNumber,
path.pathNumber,
predicateAddress,
functionSelector,
initialArguments
);
}
function _callPredicate(
Predicate memory predicate,
address player,
address tokenAddress,
uint256 tokenId
) internal view returns (bool valid) {
// If there is no predicate registered, simply return true.
if (predicate.predicateAddress == address(0)) {
return true;
}
// require(predicate.predicateAddress != address(0), "Empty predicate.");
assembly {
let starting_position := mload(0x40)
// Layout of predicate in memory:
// predicate + 0x0 : predicate + 0x20 -- predicateAddress
// predicate + 0x20 : predicate + 0x40 -- functionSelector
// predicate + 0x40 : predicate + 0x60 -- memory position of initialArguments array
//
// We store the memory position as initial_arguments_position.
// initial_arguments_position + 0x0 : initial_arguments_position + 0x20 -- length of initialArguments
//
// We use this to iterate over the initial arguments and add them to the calldata we are
// constructing.
let initial_arguments_position := mload(add(predicate, 0x40))
let initial_arguments_length := mload(initial_arguments_position)
let initial_arguments_start := add(initial_arguments_position, 0x20)
let i := 0
mstore(starting_position, mload(add(predicate, 0x20)))
let post_selector := add(starting_position, 0x4)
for {
i := 0
} lt(i, initial_arguments_length) {
i := add(i, 0x20)
} {
mstore(
add(post_selector, i),
mload(add(initial_arguments_start, i))
)
}
i := add(post_selector, initial_arguments_length)
mstore(i, player)
i := add(i, 0x20)
mstore(i, tokenAddress)
i := add(i, 0x20)
mstore(i, tokenId)
let calldata_length := add(initial_arguments_length, 0x64)
let success := staticcall(
gas(),
mload(predicate),
starting_position,
calldata_length,
add(starting_position, calldata_length),
0x20
)
if eq(success, 0) {
revert(0, returndatasize())
}
valid := mload(add(starting_position, calldata_length))
}
}
function callSessionStakingPredicate(
uint256 sessionId,
address player,
uint256 tokenId
) public view returns (bool valid) {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
address tokenAddress = gs.sessionById[sessionId].playerTokenAddress;
Predicate memory predicate = gs.sessionStakingPredicate[sessionId];
return _callPredicate(predicate, player, tokenAddress, tokenId);
}
function callPathChoicePredicate(
PathDetails memory path,
address player,
uint256 tokenId
) public view returns (bool valid) {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
address tokenAddress = gs
.sessionById[path.sessionId]
.playerTokenAddress;
Predicate memory predicate = gs.pathChoicePredicate[path.sessionId][
path.stageNumber
][path.pathNumber];
return _callPredicate(predicate, player, tokenAddress, tokenId);
}
function stakeTokensIntoSession(
uint256 sessionId,
uint256[] calldata tokenIds
) external diamondNonReentrant {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.stakeTokensIntoSession: Invalid session ID"
);
address paymentTokenAddress = gs
.sessionById[sessionId]
.paymentTokenAddress;
if (paymentTokenAddress != address(0)) {
IERC20 paymentToken = IERC20(paymentTokenAddress);
uint256 paymentAmount = gs.sessionById[sessionId].paymentAmount *
tokenIds.length;
bool paymentSuccessful = paymentToken.transferFrom(
msg.sender,
address(this),
paymentAmount
);
require(
paymentSuccessful,
"GOFPFacet.stakeTokensIntoSession: Session requires payment but payment was unsuccessful"
);
}
require(
gs.sessionById[sessionId].isActive,
"GOFPFacet.stakeTokensIntoSession: Cannot stake tokens into inactive session"
);
require(
gs.sessionStagePath[sessionId][1] == 0,
"GOFPFacet.stakeTokensIntoSession: The first stage for this session has already been resolved"
);
address nftAddress = gs.sessionById[sessionId].playerTokenAddress;
IERC721 token = IERC721(nftAddress);
for (uint256 i = 0; i < tokenIds.length; i++) {
// TODO(zomglings): Currently, Garden of Forking Paths does not allow even someone who is *approved* to transfer
// NFTs on behalf of their owners to stake those NFTs into a session.
// We may want to change this in the future. Perhaps the more correct thing would be to check if the msg.sender
// was approved by the NFT owner on the ERC721 contract.
// We should check if approvals are intended to compose transitively on ERC721.
// Just because person A gives person B approval to transfer ERC721 tokens, doesn't mean they want them to
// have permission to instigate *another* address with transfer approval to make a transfer.
require(
token.ownerOf(tokenIds[i]) == msg.sender,
"GOFPFacet.stakeTokensIntoSession: Cannot stake a token into session which is not owned by message sender"
);
require(
!gs.sessionTokenStakeGuard[sessionId][tokenIds[i]],
"GOFPFacet.stakeTokensIntoSession: Token was previously staked into session"
);
require(
callSessionStakingPredicate(sessionId, msg.sender, tokenIds[i]),
"GOFPFacet.stakeTokensIntoSession: Session staking predicate not satisfied"
);
token.safeTransferFrom(msg.sender, address(this), tokenIds[i]);
gs.sessionTokenStakeGuard[sessionId][tokenIds[i]] = true;
_addTokenToEnumeration(
sessionId,
msg.sender,
nftAddress,
tokenIds[i]
);
}
}
function unstakeTokensFromSession(
uint256 sessionId,
uint256[] calldata tokenIds
) external diamondNonReentrant {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.unstakeTokensFromSession: Invalid session ID"
);
Session storage session = gs.sessionById[sessionId];
address nftAddress = session.playerTokenAddress;
IERC721 token = IERC721(nftAddress);
for (uint256 i = 0; i < tokenIds.length; i++) {
_removeTokenFromEnumeration(
sessionId,
msg.sender,
nftAddress,
tokenIds[i]
);
token.safeTransferFrom(address(this), msg.sender, tokenIds[i]);
}
}
/**
Returns the number of the current stage.
*/
function getCurrentStage(
uint256 sessionId
) external view returns (uint256) {
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.getCurrentStage: Invalid session ID"
);
Session storage session = gs.sessionById[sessionId];
uint256 lastStage = 0;
for (uint256 i = 1; i <= session.stages.length; i++) {
if (gs.sessionStagePath[sessionId][i] > 0) {
lastStage = i;
} else {
break;
}
}
return lastStage + 1;
}
/**
For the current stage of the session with the given sessionId, a player may make a choice of paths
for each of their tokenIds.
The tokenIds array is expected to be the same length as the paths array.
A choice may only be made if choosing is currently active for the given session.
If the current stage is not the first stage, it is expected that each of the tokens specified by
tokenIds made the correct choice in the previous stage.
*/
function chooseCurrentStagePaths(
uint256 sessionId,
uint256[] memory tokenIds,
uint256[] memory paths
) external diamondNonReentrant {
require(
tokenIds.length == paths.length,
"GOFPFacet.chooseCurrentStagePaths: tokenIds and paths arrays must be of the same length"
);
LibGOFP.GOFPStorage storage gs = LibGOFP.gofpStorage();
require(
sessionId <= gs.numSessions,
"GOFPFacet.chooseCurrentStagePaths: Invalid session ID"