From 21a25f8721fa2d8951d2ae86c47ea47ed60054da Mon Sep 17 00:00:00 2001 From: Kellan Wampler Date: Tue, 26 Sep 2023 11:10:11 -0700 Subject: [PATCH 1/4] Local branch with old inventory cli. --- cli/web3cli/InventoryFacet.py | 283 +++- cli/web3cli/cli.py | 4 + contracts/inventory/IInventory.sol | 136 +- contracts/inventory/InventoryFacet.sol | 337 +++-- .../GardenOfForkingPaths.sol | 1170 ----------------- 5 files changed, 553 insertions(+), 1377 deletions(-) delete mode 100644 contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol diff --git a/cli/web3cli/InventoryFacet.py b/cli/web3cli/InventoryFacet.py index a50be9b4..1fbd9485 100644 --- a/cli/web3cli/InventoryFacet.py +++ b/cli/web3cli/InventoryFacet.py @@ -1,5 +1,5 @@ -# Code generated by moonworm : https://github.com/bugout-dev/moonworm -# Moonworm version : 0.6.2 +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 import argparse import json @@ -96,15 +96,44 @@ def verify_contract(self): contract_class = contract_from_build(self.contract_name) contract_class.publish_source(self.contract) + def add_backpack_to_subject( + self, + slot_qty: int, + to_subject_token_id: int, + slot_type: int, + slot_uri: str, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.addBackpackToSubject( + slot_qty, to_subject_token_id, slot_type, slot_uri, transaction_config + ) + def admin_terminus_info( self, block_number: Optional[Union[str, int]] = "latest" ) -> Any: self.assert_contract_is_instantiated() return self.contract.adminTerminusInfo.call(block_identifier=block_number) - def create_slot(self, persistent: bool, slot_uri: str, transaction_config) -> Any: + def assign_slot_type(self, slot: int, slot_type: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.assignSlotType(slot, slot_type, transaction_config) + + def create_slot( + self, unequippable: bool, slot_type: int, slot_uri: str, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createSlot( + unequippable, slot_type, slot_uri, transaction_config + ) + + def create_slot_type( + self, slot_type: int, slot_type_name: str, transaction_config + ) -> Any: self.assert_contract_is_instantiated() - return self.contract.createSlot(persistent, slot_uri, transaction_config) + return self.contract.createSlotType( + slot_type, slot_type_name, transaction_config + ) def equip( self, @@ -127,6 +156,25 @@ def equip( transaction_config, ) + def equip_batch( + self, subject_token_id: int, slots: List, items: List, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.equipBatch( + subject_token_id, slots, items, transaction_config + ) + + def get_all_equipped_items( + self, + subject_token_id: int, + slots: List, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getAllEquippedItems.call( + subject_token_id, slots, block_identifier=block_number + ) + def get_equipped_item( self, subject_token_id: int, @@ -144,12 +192,26 @@ def get_slot_by_id( self.assert_contract_is_instantiated() return self.contract.getSlotById.call(slot_id, block_identifier=block_number) + def get_slot_type( + self, slot_type: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getSlotType.call(slot_type, block_identifier=block_number) + def get_slot_uri( self, slot_id: int, block_number: Optional[Union[str, int]] = "latest" ) -> Any: self.assert_contract_is_instantiated() return self.contract.getSlotURI.call(slot_id, block_identifier=block_number) + def get_subject_token_slots( + self, subject_token_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.getSubjectTokenSlots.call( + subject_token_id, block_identifier=block_number + ) + def init( self, admin_terminus_address: ChecksumAddress, @@ -237,21 +299,23 @@ def on_erc721_received( arg1, arg2, arg3, arg4, transaction_config ) - def set_slot_persistent( - self, slot_id: int, persistent: bool, transaction_config + def set_slot_unequippable( + self, unquippable: bool, slot_id: int, transaction_config ) -> Any: self.assert_contract_is_instantiated() - return self.contract.setSlotPersistent(slot_id, persistent, transaction_config) + return self.contract.setSlotUnequippable( + unquippable, slot_id, transaction_config + ) def set_slot_uri(self, new_slot_uri: str, slot_id: int, transaction_config) -> Any: self.assert_contract_is_instantiated() - return self.contract.setSlotURI(new_slot_uri, slot_id, transaction_config) + return self.contract.setSlotUri(new_slot_uri, slot_id, transaction_config) - def slot_is_persistent( + def slot_is_unequippable( self, slot_id: int, block_number: Optional[Union[str, int]] = "latest" ) -> Any: self.assert_contract_is_instantiated() - return self.contract.slotIsPersistent.call( + return self.contract.slotIsUnequippable.call( slot_id, block_identifier=block_number ) @@ -365,6 +429,22 @@ def handle_verify_contract(args: argparse.Namespace) -> None: print(result) +def handle_add_backpack_to_subject(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.add_backpack_to_subject( + slot_qty=args.slot_qty, + to_subject_token_id=args.to_subject_token_id, + slot_type=args.slot_type, + slot_uri=args.slot_uri, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + def handle_admin_terminus_info(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) @@ -372,12 +452,25 @@ def handle_admin_terminus_info(args: argparse.Namespace) -> None: print(result) +def handle_assign_slot_type(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.assign_slot_type( + slot=args.slot, slot_type=args.slot_type, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + def handle_create_slot(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) transaction_config = get_transaction_config(args) result = contract.create_slot( - persistent=args.persistent, + unequippable=args.unequippable, + slot_type=args.slot_type, slot_uri=args.slot_uri, transaction_config=transaction_config, ) @@ -386,6 +479,20 @@ def handle_create_slot(args: argparse.Namespace) -> None: print(result.info()) +def handle_create_slot_type(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_slot_type( + slot_type=args.slot_type, + slot_type_name=args.slot_type_name, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + def handle_equip(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) @@ -404,6 +511,32 @@ def handle_equip(args: argparse.Namespace) -> None: print(result.info()) +def handle_equip_batch(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.equip_batch( + subject_token_id=args.subject_token_id, + slots=args.slots, + items=args.items, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_get_all_equipped_items(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + result = contract.get_all_equipped_items( + subject_token_id=args.subject_token_id, + slots=args.slots, + block_number=args.block_number, + ) + print(result) + + def handle_get_equipped_item(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) @@ -424,6 +557,15 @@ def handle_get_slot_by_id(args: argparse.Namespace) -> None: print(result) +def handle_get_slot_type(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + result = contract.get_slot_type( + slot_type=args.slot_type, block_number=args.block_number + ) + print(result) + + def handle_get_slot_uri(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) @@ -431,6 +573,15 @@ def handle_get_slot_uri(args: argparse.Namespace) -> None: print(result) +def handle_get_subject_token_slots(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = InventoryFacet(args.address) + result = contract.get_subject_token_slots( + subject_token_id=args.subject_token_id, block_number=args.block_number + ) + print(result) + + def handle_init(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) @@ -533,13 +684,13 @@ def handle_on_erc721_received(args: argparse.Namespace) -> None: print(result.info()) -def handle_set_slot_persistent(args: argparse.Namespace) -> None: +def handle_set_slot_unequippable(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) transaction_config = get_transaction_config(args) - result = contract.set_slot_persistent( + result = contract.set_slot_unequippable( + unquippable=args.unquippable, slot_id=args.slot_id, - persistent=args.persistent, transaction_config=transaction_config, ) print(result) @@ -561,10 +712,10 @@ def handle_set_slot_uri(args: argparse.Namespace) -> None: print(result.info()) -def handle_slot_is_persistent(args: argparse.Namespace) -> None: +def handle_slot_is_unequippable(args: argparse.Namespace) -> None: network.connect(args.network) contract = InventoryFacet(args.address) - result = contract.slot_is_persistent( + result = contract.slot_is_unequippable( slot_id=args.slot_id, block_number=args.block_number ) print(result) @@ -615,20 +766,59 @@ def generate_cli() -> argparse.ArgumentParser: add_default_arguments(verify_contract_parser, False) verify_contract_parser.set_defaults(func=handle_verify_contract) + add_backpack_to_subject_parser = subcommands.add_parser("add-backpack-to-subject") + add_default_arguments(add_backpack_to_subject_parser, True) + add_backpack_to_subject_parser.add_argument( + "--slot-qty", required=True, help="Type: uint256", type=int + ) + add_backpack_to_subject_parser.add_argument( + "--to-subject-token-id", required=True, help="Type: uint256", type=int + ) + add_backpack_to_subject_parser.add_argument( + "--slot-type", required=True, help="Type: uint256", type=int + ) + add_backpack_to_subject_parser.add_argument( + "--slot-uri", required=True, help="Type: string", type=str + ) + add_backpack_to_subject_parser.set_defaults(func=handle_add_backpack_to_subject) + admin_terminus_info_parser = subcommands.add_parser("admin-terminus-info") add_default_arguments(admin_terminus_info_parser, False) admin_terminus_info_parser.set_defaults(func=handle_admin_terminus_info) + assign_slot_type_parser = subcommands.add_parser("assign-slot-type") + add_default_arguments(assign_slot_type_parser, True) + assign_slot_type_parser.add_argument( + "--slot", required=True, help="Type: uint256", type=int + ) + assign_slot_type_parser.add_argument( + "--slot-type", required=True, help="Type: uint256", type=int + ) + assign_slot_type_parser.set_defaults(func=handle_assign_slot_type) + create_slot_parser = subcommands.add_parser("create-slot") add_default_arguments(create_slot_parser, True) create_slot_parser.add_argument( - "--persistent", required=True, help="Type: bool", type=boolean_argument_type + "--unequippable", required=True, help="Type: bool", type=boolean_argument_type + ) + create_slot_parser.add_argument( + "--slot-type", required=True, help="Type: uint256", type=int ) create_slot_parser.add_argument( "--slot-uri", required=True, help="Type: string", type=str ) create_slot_parser.set_defaults(func=handle_create_slot) + create_slot_type_parser = subcommands.add_parser("create-slot-type") + add_default_arguments(create_slot_type_parser, True) + create_slot_type_parser.add_argument( + "--slot-type", required=True, help="Type: uint256", type=int + ) + create_slot_type_parser.add_argument( + "--slot-type-name", required=True, help="Type: string", type=str + ) + create_slot_type_parser.set_defaults(func=handle_create_slot_type) + equip_parser = subcommands.add_parser("equip") add_default_arguments(equip_parser, True) equip_parser.add_argument( @@ -645,6 +835,29 @@ def generate_cli() -> argparse.ArgumentParser: equip_parser.add_argument("--amount", required=True, help="Type: uint256", type=int) equip_parser.set_defaults(func=handle_equip) + equip_batch_parser = subcommands.add_parser("equip-batch") + add_default_arguments(equip_batch_parser, True) + equip_batch_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + equip_batch_parser.add_argument( + "--slots", required=True, help="Type: uint256[]", nargs="+" + ) + equip_batch_parser.add_argument( + "--items", required=True, help="Type: tuple[]", nargs="+" + ) + equip_batch_parser.set_defaults(func=handle_equip_batch) + + get_all_equipped_items_parser = subcommands.add_parser("get-all-equipped-items") + add_default_arguments(get_all_equipped_items_parser, False) + get_all_equipped_items_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + get_all_equipped_items_parser.add_argument( + "--slots", required=True, help="Type: uint256[]", nargs="+" + ) + get_all_equipped_items_parser.set_defaults(func=handle_get_all_equipped_items) + get_equipped_item_parser = subcommands.add_parser("get-equipped-item") add_default_arguments(get_equipped_item_parser, False) get_equipped_item_parser.add_argument( @@ -662,6 +875,13 @@ def generate_cli() -> argparse.ArgumentParser: ) get_slot_by_id_parser.set_defaults(func=handle_get_slot_by_id) + get_slot_type_parser = subcommands.add_parser("get-slot-type") + add_default_arguments(get_slot_type_parser, False) + get_slot_type_parser.add_argument( + "--slot-type", required=True, help="Type: uint256", type=int + ) + get_slot_type_parser.set_defaults(func=handle_get_slot_type) + get_slot_uri_parser = subcommands.add_parser("get-slot-uri") add_default_arguments(get_slot_uri_parser, False) get_slot_uri_parser.add_argument( @@ -669,6 +889,13 @@ def generate_cli() -> argparse.ArgumentParser: ) get_slot_uri_parser.set_defaults(func=handle_get_slot_uri) + get_subject_token_slots_parser = subcommands.add_parser("get-subject-token-slots") + add_default_arguments(get_subject_token_slots_parser, False) + get_subject_token_slots_parser.add_argument( + "--subject-token-id", required=True, help="Type: uint256", type=int + ) + get_subject_token_slots_parser.set_defaults(func=handle_get_subject_token_slots) + init_parser = subcommands.add_parser("init") add_default_arguments(init_parser, True) init_parser.add_argument( @@ -783,15 +1010,15 @@ def generate_cli() -> argparse.ArgumentParser: ) on_erc721_received_parser.set_defaults(func=handle_on_erc721_received) - set_slot_persistent_parser = subcommands.add_parser("set-slot-persistent") - add_default_arguments(set_slot_persistent_parser, True) - set_slot_persistent_parser.add_argument( - "--slot-id", required=True, help="Type: uint256", type=int + set_slot_unequippable_parser = subcommands.add_parser("set-slot-unequippable") + add_default_arguments(set_slot_unequippable_parser, True) + set_slot_unequippable_parser.add_argument( + "--unquippable", required=True, help="Type: bool", type=boolean_argument_type ) - set_slot_persistent_parser.add_argument( - "--persistent", required=True, help="Type: bool", type=boolean_argument_type + set_slot_unequippable_parser.add_argument( + "--slot-id", required=True, help="Type: uint256", type=int ) - set_slot_persistent_parser.set_defaults(func=handle_set_slot_persistent) + set_slot_unequippable_parser.set_defaults(func=handle_set_slot_unequippable) set_slot_uri_parser = subcommands.add_parser("set-slot-uri") add_default_arguments(set_slot_uri_parser, True) @@ -803,12 +1030,12 @@ def generate_cli() -> argparse.ArgumentParser: ) set_slot_uri_parser.set_defaults(func=handle_set_slot_uri) - slot_is_persistent_parser = subcommands.add_parser("slot-is-persistent") - add_default_arguments(slot_is_persistent_parser, False) - slot_is_persistent_parser.add_argument( + slot_is_unequippable_parser = subcommands.add_parser("slot-is-unequippable") + add_default_arguments(slot_is_unequippable_parser, False) + slot_is_unequippable_parser.add_argument( "--slot-id", required=True, help="Type: uint256", type=int ) - slot_is_persistent_parser.set_defaults(func=handle_slot_is_persistent) + slot_is_unequippable_parser.set_defaults(func=handle_slot_is_unequippable) subject_parser = subcommands.add_parser("subject") add_default_arguments(subject_parser, False) diff --git a/cli/web3cli/cli.py b/cli/web3cli/cli.py index de14a558..0c870a58 100644 --- a/cli/web3cli/cli.py +++ b/cli/web3cli/cli.py @@ -19,6 +19,7 @@ InventoryFacet, TerminusFacet, StatBlock, + moonbound, ) @@ -87,6 +88,9 @@ def main() -> None: statblock_parser = StatBlock.generate_cli() subparsers.add_parser("statblock", parents=[statblock_parser], add_help=False) + moonbound_parser = moonbound.generate_cli() + subparsers.add_parser("moonbound", parents=[moonbound_parser], add_help=False) + args = parser.parse_args() args.func(args) diff --git a/contracts/inventory/IInventory.sol b/contracts/inventory/IInventory.sol index e8e6a9fc..d7b81b97 100644 --- a/contracts/inventory/IInventory.sol +++ b/contracts/inventory/IInventory.sol @@ -1,33 +1,28 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; -struct Slot { - string SlotURI; - bool SlotIsPersistent; -} - -// EquippedItem represents an item equipped in a specific inventory slot for a specific ERC721 token. -struct EquippedItem { - uint256 ItemType; - address ItemAddress; - uint256 ItemTokenId; - uint256 Amount; -} +import "./LibInventory.sol"; -// Interface ID: 6e34096c -// -// Calculated by solface: https://github.com/moonstream-to/solface -// -// To recalculate from root directory of this repo: -// $ jq .abi build/contracts/IInventory.json | solface -name IInventory -annotations | grep "Interface ID:" -// -// Note: Change path to build/contracts/IInventory.json depending on where you are relative to the repo root. interface IInventory { - // This event should be emitted when the subject ERC721 contract address is set (or changes) on the - // Inventory contract. - event NewSubjectAddress(address indexed contractAddress); + event AdministratorDesignated( + address indexed adminTerminusAddress, + uint256 indexed adminTerminusPoolId + ); + + event ContractAddressDesignated(address indexed contractAddress); - event SlotCreated(address indexed creator, uint256 slot); + event SlotCreated( + address indexed creator, + uint256 indexed slot, + bool unequippable, + uint256 indexed slotType + ); + + event NewSlotTypeAdded( + address indexed creator, + uint256 indexed slotType, + string slotTypeName + ); event ItemMarkedAsEquippableInSlot( uint256 indexed slot, @@ -37,9 +32,19 @@ interface IInventory { uint256 maxAmount ); + event BackpackAdded( + address indexed creator, + uint256 indexed toSubjectTokenId, + uint256 indexed slotQuantity + ); + event NewSlotURI(uint256 indexed slotId); - event NewSlotPersistence(uint256 indexed slotId, bool persistent); + event SlotTypeAdded( + address indexed creator, + uint256 indexed slotId, + uint256 indexed slotType + ); event ItemEquipped( uint256 indexed subjectTokenId, @@ -61,37 +66,26 @@ interface IInventory { address unequippedBy ); + function init( + address adminTerminusAddress, + uint256 adminTerminusPoolId, + address subjectAddress + ) external; + function adminTerminusInfo() external view returns (address, uint256); function subject() external view returns (address); - // Constraint: Admin - // Emits: SlotCreated, NewSlotURI, NewSlotPersistence function createSlot( - bool persistent, + bool unequippable, + uint256 slotType, string memory slotURI ) external returns (uint256); function numSlots() external view returns (uint256); - function getSlotById( - uint256 slotId - ) external view returns (Slot memory slots); - - function getSlotURI(uint256 slotId) external view returns (string memory); - - function slotIsPersistent(uint256 slotId) external view returns (bool); - - // Constraint: Admin - // Emits: NewSlotURI - function setSlotURI(string memory newSlotURI, uint slotId) external; + function slotIsUnequippable(uint256 slotId) external view returns (bool); - // Constraint: Admin - // Emits: NewSlotPersistence - function setSlotPersistent(uint256 slotId, bool persistent) external; - - // Constraint: Admin - // Emits: ItemMarkedAsEquippableInSlot function markItemAsEquippableInSlot( uint256 slot, uint256 itemType, @@ -107,9 +101,6 @@ interface IInventory { uint256 itemPoolId ) external view returns (uint256); - // Constraint: Non-reentrant. - // Emits: ItemEquipped - // Optionally emits: ItemUnequipped (if the current item in that slot is being replaced) function equip( uint256 subjectTokenId, uint256 slot, @@ -119,8 +110,6 @@ interface IInventory { uint256 amount ) external; - // Constraint: Non-reentrant. - // Emits: ItemUnequipped function unequip( uint256 subjectTokenId, uint256 slot, @@ -131,5 +120,46 @@ interface IInventory { function getEquippedItem( uint256 subjectTokenId, uint256 slot - ) external view returns (EquippedItem memory item); + ) external view returns (LibInventory.EquippedItem memory item); + + function getSlotById( + uint256 slotId + ) external view returns (LibInventory.Slot memory slots); + + function getSubjectTokenSlots( + uint256 subjectTokenId + ) external view returns (LibInventory.Slot[] memory slot); + + function addBackpackToSubject( + uint256 slotQty, + uint256 toSubjectTokenId, + uint256 slotType, + string memory slotURI + ) external; + + function getSlotURI(uint256 slotId) external view returns (string memory); + + function createSlotType( + uint256 slotType, + string memory slotTypeName + ) external; + + function assignSlotType(uint256 slot, uint256 slotType) external; + + function getSlotType( + uint256 slotType + ) external view returns (string memory slotTypeName); + + function setSlotUnequippable(bool unquippable, uint256 slotId) external; + + function getAllEquippedItems( + uint256 subjectTokenId, + uint256[] memory slots + ) external view returns (LibInventory.EquippedItem[] memory equippedItems); + + function equipBatch( + uint256 subjectTokenId, + uint256[] memory slots, + LibInventory.EquippedItem[] memory items + ) external; } diff --git a/contracts/inventory/InventoryFacet.sol b/contracts/inventory/InventoryFacet.sol index 14308f91..5f611f8b 100644 --- a/contracts/inventory/InventoryFacet.sol +++ b/contracts/inventory/InventoryFacet.sol @@ -1,78 +1,22 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT /** - * Authors: Moonstream Engineering (engineering@moonstream.to) - * GitHub: https://github.com/moonstream-to/web3 + * Authors: Omar Garcia, Moonstream DAO (engineering@moonstream.to) + * GitHub: https://github.com/G7DAO/contracts */ -pragma solidity ^0.8.0; +pragma solidity ^0.8.17; +import {TerminusPermissions} from "../terminus/TerminusPermissions.sol"; +import {DiamondReentrancyGuard} from "../diamond/security/DiamondReentrancyGuard.sol"; 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 "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; import "../diamond/libraries/LibDiamond.sol"; -import {DiamondReentrancyGuard} from "../diamond/security/DiamondReentrancyGuard.sol"; -import {Slot, EquippedItem, IInventory} from "./IInventory.sol"; -import {TerminusPermissions} from "../terminus/TerminusPermissions.sol"; - -/** -LibInventory defines the storage structure used by the Inventory contract as a facet for an EIP-2535 Diamond -proxy. - */ -library LibInventory { - bytes32 constant STORAGE_POSITION = - keccak256("moonstreamdao.eth.storage.Inventory"); - - uint256 constant ERC20_ITEM_TYPE = 20; - uint256 constant ERC721_ITEM_TYPE = 721; - uint256 constant ERC1155_ITEM_TYPE = 1155; - - struct InventoryStorage { - address AdminTerminusAddress; - uint256 AdminTerminusPoolId; - address ContractERC721Address; - uint256 NumSlots; - // SlotId => slot data (URI, persistence) - mapping(uint256 => Slot) SlotData; - // Slot => item type => item address => item pool ID => maximum equippable - // For ERC20 and ERC721 tokens, item pool ID is assumed to be 0. No data will be stored under positive - // item pool IDs. - // - // NOTE: It is possible for the same contract to implement multiple of these ERCs (e.g. ERC20 and ERC721), - // so this data structure actually makes sense. - mapping(uint256 => mapping(uint256 => mapping(address => mapping(uint256 => uint256)))) SlotEligibleItems; - // Subject contract address => subject token ID => slot => EquippedItem - // Item type and Pool ID on EquippedItem have the same constraints as they do elsewhere (e.g. in SlotEligibleItems). - // - // NOTE: We have added the subject contract address as the first mapping key as a defense against - // future modifications which may allow administrators to modify the subject contract address. - // If such a modification were made, it could make it possible for a bad actor administrator - // to change the address of the subject token to the address to an ERC721 contract they control - // and drain all items from every subject token's inventory. - // If this contract is deployed as a Diamond proxy, the owner of the Diamond can pretty much - // do whatever they want in any case, but adding the subject contract address as a key protects - // users of non-Diamond deployments even under small variants of the current implementation. - // It also offers *some* protection to users of Diamond deployments of the Inventory. - // ERC721 Contract Address => - // subjectTokenId => - // slotId => - // EquippedItem struct - mapping(address => mapping(uint256 => mapping(uint256 => EquippedItem))) EquippedItems; - } - - function inventoryStorage() - internal - pure - returns (InventoryStorage storage istore) - { - bytes32 position = STORAGE_POSITION; - assembly { - istore.slot := position - } - } -} +import "./LibInventory.sol"; +import "./IInventory.sol"; /** InventoryFacet is a smart contract that can either be used standalone or as part of an EIP-2535 Diamond @@ -89,10 +33,10 @@ Admin flow: - [x] Define tokens as equippable in inventory slots Player flow: -- [x] Equip ERC20 tokens in eligible inventory slots -- [x] Equip ERC721 tokens in eligible inventory slots -- [x] Equip ERC1155 tokens in eligible inventory slots -- [x] Unequip items from persistent slots +- [] Equip ERC20 tokens in eligible inventory slots +- [] Equip ERC721 tokens in eligible inventory slots +- [] Equip ERC1155 tokens in eligible inventory slots +- [ ] Unequip items from unequippable slots Batch endpoints: - [ ] Marking items as equippable @@ -106,11 +50,6 @@ contract InventoryFacet is TerminusPermissions, DiamondReentrancyGuard { - event AdministratorDesignated( - address indexed adminTerminusAddress, - uint256 indexed adminTerminusPoolId - ); - modifier onlyAdmin() { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); @@ -125,6 +64,27 @@ contract InventoryFacet is _; } + modifier requireValidItemType(uint256 itemType) { + require( + itemType == LibInventory.ERC20_ITEM_TYPE || + itemType == LibInventory.ERC721_ITEM_TYPE || + itemType == LibInventory.ERC1155_ITEM_TYPE, + "InventoryFacet.requireValidItemType: Invalid item type" + ); + _; + } + + modifier onlyContractSubjectOwner(uint256 subjectTokenId) { + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + IERC721 subjectContract = IERC721(istore.ContractERC721Address); + require( + msg.sender == subjectContract.ownerOf(subjectTokenId), + "InventoryFacet.getSubjectTokenSlots: Message sender is not owner of subject token" + ); + _; + } + /** An Inventory must be initialized with: 1. adminTerminusAddress: The address for the Terminus contract which hosts the Administrator badge. @@ -144,7 +104,7 @@ contract InventoryFacet is istore.ContractERC721Address = contractAddress; emit AdministratorDesignated(adminTerminusAddress, adminTerminusPoolId); - emit NewSubjectAddress(contractAddress); + emit ContractAddressDesignated(contractAddress); } function adminTerminusInfo() external view returns (address, uint256) { @@ -158,7 +118,8 @@ contract InventoryFacet is } function createSlot( - bool persistent, + bool unequippable, + uint256 slotType, string memory slotURI ) external onlyAdmin returns (uint256) { LibInventory.InventoryStorage storage istore = LibInventory @@ -168,24 +129,113 @@ contract InventoryFacet is istore.NumSlots += 1; uint256 newSlot = istore.NumSlots; // save the slot type! - istore.SlotData[newSlot] = Slot({ + istore.SlotData[newSlot] = LibInventory.Slot({ + SlotType: slotType, SlotURI: slotURI, - SlotIsPersistent: persistent + SlotIsUnequippable: unequippable, + SlotId: newSlot }); - emit SlotCreated(msg.sender, newSlot); - emit NewSlotURI(newSlot); - emit NewSlotPersistence(newSlot, persistent); + emit SlotCreated(msg.sender, newSlot, unequippable, slotType); return newSlot; } + function createSlotType( + uint256 slotType, + string memory slotTypeName + ) external onlyAdmin { + require( + bytes(slotTypeName).length > 0, + "InventoryFacet.setSlotType: Slot type name must be non-empty" + ); + require( + slotType > 0, + "InventoryFacet.setSlotType: Slot type must be greater than 0" + ); + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + istore.SlotTypes[slotType] = slotTypeName; + emit NewSlotTypeAdded(msg.sender, slotType, slotTypeName); + } + + function assignSlotType(uint256 slot, uint256 slotType) external onlyAdmin { + require( + slotType > 0, + "InventoryFacet.addSlotType: SlotType must be greater than 0" + ); + + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + istore.SlotData[slot].SlotType = slotType; + emit SlotTypeAdded(msg.sender, slot, slotType); + } + + function getSlotType( + uint256 slotType + ) external view returns (string memory slotTypeName) { + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + return istore.SlotTypes[slotType]; + } + + // TODO: @ogarciarevett change this to use a external backpack NFT + function addBackpackToSubject( + uint256 slotQty, + uint256 toSubjectTokenId, + uint256 slotType, + string memory slotURI + ) external onlyAdmin { + require( + slotQty > 0, + "InventoryFacet.addBackpackToSubject: Slot quantity must be greater than 0" + ); + + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + + uint256 previousSlotNumSubject = istore + .SubjectSlots[istore.ContractERC721Address][toSubjectTokenId].length; + + for (uint256 i = 0; i < slotQty; i++) { + istore + .SubjectSlots[istore.ContractERC721Address][toSubjectTokenId].push( + LibInventory.Slot({ + SlotType: slotType, + SlotURI: slotURI, + SlotIsUnequippable: false, + SlotId: previousSlotNumSubject + i == + previousSlotNumSubject + ? previousSlotNumSubject + 1 + : previousSlotNumSubject + i + }) + ); + } + + emit BackpackAdded(msg.sender, toSubjectTokenId, slotQty); + } + + function getSubjectTokenSlots( + uint256 subjectTokenId + ) + external + view + onlyContractSubjectOwner(subjectTokenId) + returns (LibInventory.Slot[] memory slots) + { + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + return + istore.SubjectSlots[istore.ContractERC721Address][subjectTokenId]; + } + + // COUNTER function numSlots() external view returns (uint256) { return LibInventory.inventoryStorage().NumSlots; } function getSlotById( uint256 slotId - ) external view returns (Slot memory slot) { + ) external view returns (LibInventory.Slot memory slot) { return LibInventory.inventoryStorage().SlotData[slotId]; } @@ -196,36 +246,34 @@ contract InventoryFacet is return istore.SlotData[slotId].SlotURI; } - function setSlotURI( + function setSlotUri( string memory newSlotURI, uint slotId ) external onlyAdmin { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); - Slot memory slot = istore.SlotData[slotId]; + LibInventory.Slot memory slot = istore.SlotData[slotId]; slot.SlotURI = newSlotURI; istore.SlotData[slotId] = slot; emit NewSlotURI(slotId); } - function slotIsPersistent(uint256 slotId) external view returns (bool) { + function slotIsUnequippable(uint256 slotId) external view returns (bool) { return - LibInventory.inventoryStorage().SlotData[slotId].SlotIsPersistent; + LibInventory.inventoryStorage().SlotData[slotId].SlotIsUnequippable; } - function setSlotPersistent( - uint256 slotId, - bool persistent + function setSlotUnequippable( + bool unquippable, + uint256 slotId ) external onlyAdmin { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); - Slot memory slot = istore.SlotData[slotId]; - slot.SlotIsPersistent = persistent; + LibInventory.Slot memory slot = istore.SlotData[slotId]; + slot.SlotIsUnequippable = unquippable; istore.SlotData[slotId] = slot; - - emit NewSlotPersistence(slotId, persistent); } function markItemAsEquippableInSlot( @@ -234,14 +282,7 @@ contract InventoryFacet is address itemAddress, uint256 itemPoolId, uint256 maxAmount - ) external onlyAdmin { - require( - itemType == LibInventory.ERC20_ITEM_TYPE || - itemType == LibInventory.ERC721_ITEM_TYPE || - itemType == LibInventory.ERC1155_ITEM_TYPE, - "InventoryFacet.markItemAsEquippableInSlot: Invalid item type" - ); - + ) external onlyAdmin requireValidItemType(itemType) { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); @@ -304,11 +345,11 @@ contract InventoryFacet is .inventoryStorage(); require( - !istore.SlotData[slot].SlotIsPersistent, - "InventoryFacet._unequip: That slot is persistent. You cannot unequip items from it." + istore.SlotData[slot].SlotIsUnequippable, + "InventoryFacet._unequip: That slot is not unequippable" ); - EquippedItem storage existingItem = istore.EquippedItems[ + LibInventory.EquippedItem storage existingItem = istore.EquippedItems[ istore.ContractERC721Address ][subjectTokenId][slot]; @@ -371,20 +412,14 @@ contract InventoryFacet is address itemAddress, uint256 itemTokenId, uint256 amount - ) external diamondNonReentrant { - require( - itemType == LibInventory.ERC20_ITEM_TYPE || - itemType == LibInventory.ERC721_ITEM_TYPE || - itemType == LibInventory.ERC1155_ITEM_TYPE, - "InventoryFacet.equip: Invalid item type" - ); - + ) external requireValidItemType(itemType) diamondNonReentrant { require( itemType == LibInventory.ERC721_ITEM_TYPE || itemType == LibInventory.ERC1155_ITEM_TYPE || itemTokenId == 0, "InventoryFacet.equip: itemTokenId can only be non-zero for ERC721 or ERC1155 items" ); + require( itemType == LibInventory.ERC20_ITEM_TYPE || itemType == LibInventory.ERC1155_ITEM_TYPE || @@ -405,10 +440,6 @@ contract InventoryFacet is // increasing the amount of an existing token in the given slot. To increase gas-efficiency, // we could add more complex logic here to handle that situation by only equipping the difference // between the existing amount of the token and the target amount. - // TODO(zomglings): The current implementation makes it so that players cannot increase the - // number of tokens of a given type that are equipped into a persistent slot. I would consider - // this a bug. For more details, see comment at bottom of the following test: - // web3cli.test_inventory.TestPlayerFlow.test_player_cannot_unequip_erc20_tokens_from_persistent_slot_but_can_increase_amount if ( istore .EquippedItems[istore.ContractERC721Address][subjectTokenId][slot] @@ -466,15 +497,6 @@ contract InventoryFacet is ); } - istore.EquippedItems[istore.ContractERC721Address][subjectTokenId][ - slot - ] = EquippedItem({ - ItemType: itemType, - ItemAddress: itemAddress, - ItemTokenId: itemTokenId, - Amount: amount - }); - emit ItemEquipped( subjectTokenId, slot, @@ -484,6 +506,15 @@ contract InventoryFacet is amount, msg.sender ); + + istore.EquippedItems[istore.ContractERC721Address][subjectTokenId][ + slot + ] = LibInventory.EquippedItem({ + ItemType: itemType, + ItemAddress: itemAddress, + ItemTokenId: itemTokenId, + Amount: amount + }); } function unequip( @@ -507,7 +538,7 @@ contract InventoryFacet is function getEquippedItem( uint256 subjectTokenId, uint256 slot - ) external view returns (EquippedItem memory item) { + ) external view returns (LibInventory.EquippedItem memory item) { LibInventory.InventoryStorage storage istore = LibInventory .inventoryStorage(); @@ -516,10 +547,64 @@ contract InventoryFacet is "InventoryFacet.getEquippedItem: Slot does not exist" ); - EquippedItem memory equippedItem = istore.EquippedItems[ + LibInventory.EquippedItem memory equippedItem = istore.EquippedItems[ istore.ContractERC721Address ][subjectTokenId][slot]; return equippedItem; } + + function getAllEquippedItems( + uint256 subjectTokenId, + uint256[] memory slots + ) external view returns (LibInventory.EquippedItem[] memory equippedItems) { + LibInventory.InventoryStorage storage istore = LibInventory + .inventoryStorage(); + + LibInventory.EquippedItem[] + memory items = new LibInventory.EquippedItem[](slots.length); + + for (uint256 i = 0; i < slots.length; i++) { + require( + slots[i] <= this.numSlots(), + "InventoryFacet.getEquippedItem: Slot does not exist" + ); + LibInventory.EquippedItem memory equippedItem = istore + .EquippedItems[istore.ContractERC721Address][subjectTokenId][ + slots[i] + ]; + items[i] = equippedItem; + } + + return items; + } + + function equipBatch( + uint256 subjectTokenId, + uint256[] memory slots, + LibInventory.EquippedItem[] memory items + ) external diamondNonReentrant { + require( + items.length > 0, + "InventoryFacet.batchEquip: Must equip at least one item" + ); + require( + slots.length == items.length, + "InventoryFacet.batchEquip: Must provide a slot for each item" + ); + for (uint256 i = 0; i < items.length; i++) { + require( + slots[i] <= this.numSlots(), + "InventoryFacet.batchEquip: Slot does not exist" + ); + this.equip( + subjectTokenId, + slots[i], + items[i].ItemType, + items[i].ItemAddress, + items[i].ItemTokenId, + items[i].Amount + ); + } + } } diff --git a/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol b/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol deleted file mode 100644 index daae03eb..00000000 --- a/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol +++ /dev/null @@ -1,1170 +0,0 @@ -// 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 "../../diamond/libraries/LibDiamond.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 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" - ); - - Session storage session = gs.sessionById[sessionId]; - // This prevents players from claiming rewards for path choice in inactive sessions. - // It is a form of protection for game masters - they can shut down all reward claims from the - // Garden of Forking Paths session my marking that session as inactive. - require( - session.isActive, - "GOFPFacet.chooseCurrentStagePaths: Cannot choose paths in inactive session" - ); - require( - session.isChoosingActive, - "GOFPFacet.chooseCurrentStagePaths: Cannot choose paths in a session for which choosing is not active" - ); - - uint256 i = 0; - - uint256 lastStage = 0; - uint256 lastStageCorrectPath = 0; - - for (i = 1; i <= session.stages.length; i++) { - if (gs.sessionStagePath[sessionId][i] > 0) { - lastStage = i; - lastStageCorrectPath = gs.sessionStagePath[sessionId][i]; - } else { - break; - } - } - - require( - lastStage < session.stages.length, - "GOFPFacet.chooseCurrentStagePaths: This session has ended" - ); - - uint256 currentStage = lastStage + 1; - // lastStage has no semantic meaning below. It would be more correct to say currentStage - 1. - // This is just for convenience, saving one subtraction. - - uint256 stageRewardAmount = 0; - Reward memory stageReward = gs.sessionStageReward[sessionId][ - currentStage - ]; - - for (i = 0; i < tokenIds.length; i++) { - // BEWARE: Setting tokenId and path variables resuls in a "Stack too deep" error message - // when compiling this contract. Using calldata array arguments restricts the amount of - // stack variables available to us inside the function body. - require( - gs.stakedTokenIndex[sessionId][tokenIds[i]] > 0, - "GOFPFacet.chooseCurrentStagePaths: Token not currently staked into session" - ); - require( - gs.stakedTokenOwner[session.playerTokenAddress][tokenIds[i]] == - msg.sender, - "GOFPFacet.chooseCurrentStagePaths: Message not sent by token owner" - ); - require( - (lastStage == 0) || - (session.isForgiving) || - (gs.pathChoices[sessionId][tokenIds[i]][lastStage] == - lastStageCorrectPath), - "GOFPFacet.chooseCurrentStagePaths: Session is not forgiving and token did not choose correct path in last stage" - ); - require( - gs.pathChoices[sessionId][tokenIds[i]][currentStage] == 0, - "GOFPFacet.chooseCurrentStagePaths: Token has already chosen a path in the current stage" - ); - require( - (paths[i] >= 1) && (paths[i] <= session.stages[lastStage]), - "GOFPFacet.chooseCurrentStagePaths: Invalid path" - ); - require( - callPathChoicePredicate( - PathDetails({ - sessionId: sessionId, - stageNumber: currentStage, - pathNumber: paths[i] - }), - msg.sender, - tokenIds[i] - ), - "GOFPFacet.chooseCurrentStagePaths: Path choice predicate not satisfied" - ); - gs.pathChoices[sessionId][tokenIds[i]][currentStage] = paths[i]; - emit PathChosen(sessionId, tokenIds[i], currentStage, paths[i]); - - // Calculate number of correct choices on last stage for reward distribution, but only if - // there is a stage reward. - if (stageReward.rewardAddress != address(0)) { - if ( - lastStage == 0 || - gs.pathChoices[sessionId][tokenIds[i]][lastStage] == - lastStageCorrectPath - ) { - if (stageReward.inventoryAddress == address(0)) { - stageRewardAmount += stageReward.rewardAmount; - } else { - _distributeReward(stageReward, tokenIds[i]); - } - } - } - - // Distribute path reward. - Reward memory pathReward = gs.sessionPathReward[sessionId][ - currentStage - ][paths[i]]; - _distributeReward(pathReward, tokenIds[i]); - } - - if (stageReward.inventoryAddress == address(0)) { - stageReward.rewardAmount = stageRewardAmount; - _distributeReward(stageReward, 0); - } - } - - function _distributeReward(Reward memory reward, uint256 tokenId) internal { - if (reward.rewardAddress != address(0)) { - if (reward.rewardType == TERMINUS_MINTABLE_TYPE) { - TerminusFacet rewardTerminus = TerminusFacet( - reward.rewardAddress - ); - if (reward.inventoryAddress != address(0)) { - rewardTerminus.mint( - address(this), - reward.rewardTokenID, - reward.rewardAmount, - "" - ); - _equipInventoryReward(reward, tokenId); - } else { - rewardTerminus.mint( - msg.sender, - reward.rewardTokenID, - reward.rewardAmount, - "" - ); - } - } else { - revert("Non-terminus rewards are not yet implemented"); - } - } - } - - function _equipInventoryReward( - Reward memory reward, - uint256 tokenId - ) internal { - InventoryFacet inventoryFacet = InventoryFacet(reward.inventoryAddress); - uint256 slotId = reward.inventorySlot; - bool persistent = inventoryFacet.slotIsPersistent(slotId); - if (persistent) { - inventoryFacet.setSlotPersistent(slotId, false); - } - try - inventoryFacet.equip( - tokenId, - slotId, - 1155, - reward.rewardAddress, - reward.rewardTokenID, - reward.rewardAmount - ) - {} catch Error(string memory reason) { - emit InventoryEquipError(reason); - } - if (persistent) { - inventoryFacet.setSlotPersistent(slotId, true); - } - } -} From 1ec7409f4ca80fa4d5a1f1d7c64ea9d2315d729e Mon Sep 17 00:00:00 2001 From: Kellan Wampler Date: Thu, 5 Oct 2023 10:17:50 -0400 Subject: [PATCH 2/4] Removing moonbound cli. --- cli/web3cli/cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/web3cli/cli.py b/cli/web3cli/cli.py index 0c870a58..de14a558 100644 --- a/cli/web3cli/cli.py +++ b/cli/web3cli/cli.py @@ -19,7 +19,6 @@ InventoryFacet, TerminusFacet, StatBlock, - moonbound, ) @@ -88,9 +87,6 @@ def main() -> None: statblock_parser = StatBlock.generate_cli() subparsers.add_parser("statblock", parents=[statblock_parser], add_help=False) - moonbound_parser = moonbound.generate_cli() - subparsers.add_parser("moonbound", parents=[moonbound_parser], add_help=False) - args = parser.parse_args() args.func(args) From 1e6b2170c5c2e450ab517856e934b88f13ab6543 Mon Sep 17 00:00:00 2001 From: Kellan Wampler Date: Thu, 5 Oct 2023 10:20:20 -0400 Subject: [PATCH 3/4] Updating slot type start index. --- cli/web3cli/flows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/web3cli/flows.py b/cli/web3cli/flows.py index ac44fab3..eb3d0613 100644 --- a/cli/web3cli/flows.py +++ b/cli/web3cli/flows.py @@ -100,7 +100,7 @@ def handle_create_inventory_slots_from_config(args: argparse.Namespace) -> None: # No good way to search the existing slot types. This just starts creating slot types from id 11. Perhaps it could be a parameter, but really # needs a contract change to support slot type creation. - next_slot_type_id = 11 + next_slot_type_id = 21 for item in config: slot_type = item["type"] if not slot_type in slot_type_mapping: From 83842bcf714d2cf0037bac2c01bea2867fdaffda Mon Sep 17 00:00:00 2001 From: Kellan Wampler Date: Fri, 20 Oct 2023 10:51:23 -0400 Subject: [PATCH 4/4] Missing a library for old inventory. --- contracts/inventory/LibInventory.sol | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 contracts/inventory/LibInventory.sol diff --git a/contracts/inventory/LibInventory.sol b/contracts/inventory/LibInventory.sol new file mode 100644 index 00000000..32ca0b73 --- /dev/null +++ b/contracts/inventory/LibInventory.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/** +LibInventory defines the storage structure used by the Inventory contract as a facet for an EIP-2535 Diamond +proxy. + */ +library LibInventory { + bytes32 constant STORAGE_POSITION = + keccak256("g7dao.eth.storage.Inventory"); + + uint256 constant ERC20_ITEM_TYPE = 20; + uint256 constant ERC721_ITEM_TYPE = 721; + uint256 constant ERC1155_ITEM_TYPE = 1155; + + struct Slot { + string SlotURI; + uint256 SlotType; + bool SlotIsUnequippable; + uint256 SlotId; + } + + // EquippedItem represents an item equipped in a specific inventory slot for a specific ERC721 token. + struct EquippedItem { + uint256 ItemType; + address ItemAddress; + uint256 ItemTokenId; + uint256 Amount; + } + + struct InventoryStorage { + address AdminTerminusAddress; + uint256 AdminTerminusPoolId; + address ContractERC721Address; + uint256 NumSlots; + // SlotId => slot, useful to get the rest of the slot data. + mapping(uint256 => Slot) SlotData; + // SlotType => "slot type name" + mapping(uint256 => string) SlotTypes; + // Slot => item type => item address => item pool ID => maximum equippable + // For ERC20 and ERC721 tokens, item pool ID is assumed to be 0. No data will be stored under positive + // item pool IDs. + // + // NOTE: It is possible for the same contract to implement multiple of these ERCs (e.g. ERC20 and ERC721), + // so this data structure actually makes sense. + mapping(uint256 => mapping(uint256 => mapping(address => mapping(uint256 => uint256)))) SlotEligibleItems; + // Subject contract address => subject token ID => slot => EquippedItem + // Item type and Pool ID on EquippedItem have the same constraints as they do elsewhere (e.g. in SlotEligibleItems). + // + // NOTE: We have added the subject contract address as the first mapping key as a defense against + // future modifications which may allow administrators to modify the subject contract address. + // If such a modification were made, it could make it possible for a bad actor administrator + // to change the address of the subject token to the address to an ERC721 contract they control + // and drain all items from every subject token's inventory. + // If this contract is deployed as a Diamond proxy, the owner of the Diamond can pretty much + // do whatever they want in any case, but adding the subject contract address as a key protects + // users of non-Diamond deployments even under small variants of the current implementation. + // It also offers *some* protection to users of Diamond deployments of the Inventory. + // ERC721 Contract Address => + // subjectTokenId => + // slotId => + // EquippedItem struct + mapping(address => mapping(uint256 => mapping(uint256 => EquippedItem))) EquippedItems; + // Subject contract address => subject token ID => Slot[] + mapping(address => mapping(uint256 => Slot[])) SubjectSlots; + // Subject contract address => subject token ID => slotNum + mapping(address => mapping(uint256 => uint256)) SubjectNumSlots; + // Subject contract address => subject token ID => slotId => bool + mapping(address => mapping(uint256 => mapping(uint256 => bool))) IsSubjectTokenBlackListedForSlot; + } + + function inventoryStorage() + internal + pure + returns (InventoryStorage storage istore) + { + bytes32 position = STORAGE_POSITION; + assembly { + istore.slot := position + } + } +}