// SPDX-License-Identifier: MIT
//
// DungeonConfig by DungeonMaster/@DungeonSpawner
pragma solidity ^0.8.0;
import "./Ownable.sol";
interface IDungeon {
function ownerOf(uint256 tokenId) external view returns (address owner);
}
interface IDungeonRewards {
function getStakedTokens(
address owner
)
external
view
returns (uint256[] memory dungeons, uint256[] memory avatars);
}
contract DungeonConfig is Ownable {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event AssignPerms(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
event AssignGlobalPerms(address indexed from, address indexed to);
event RevokeGlobalPerms(address indexed from, address indexed to);
/*///////////////////////////////////////////////////////////////
METADATA STORAGE/LOGIC
//////////////////////////////////////////////////////////////*/
address public dungeonAddress;
address public dungeonRewardsAddress;
struct Permission {
address grantedBy; // The address that granted the permission
address grantedTo; // The address that received the permission
}
struct GlobalPermission {
address grantedTo;
address grantedBy;
}
struct FloorData {
string txId; // transaction ID for the floor data
uint256 version; // current version of the floor data (every resetAllFloors increments the version #, outdating previous entries)
}
struct DungeonData {
bool locked;
uint256 trialTimeout;
bool randomizeLayout; // randomize layout indicates each floor after floor 1 has a random layout/environment
string passwordHash; // DungeonMaster notes: not really secure, of course, since it's publically available, but the best we have
string ownersMessage;
string gameMode; // space theme, 3d, leaderboard, etc... and if starts with https:, NFT will redirect to url after loading up (use for streaming via rumble, etc)
string tilesetOverride; // override tileset with this txid/url allowing owners further customization
}
// Mapping that tells us which version of the dungeon data we're dealing with
// (reseting all floors increments the version # -- saves us gas from having to clear all the existing dungeon floor data)
mapping(uint256 => uint256) private tokenVersions;
mapping(uint256 => DungeonData) public dungeons;
// Mapping from tokenId to a mapping of floorNumber to FloorData
mapping(uint256 => mapping(uint256 => FloorData)) public dungeonFloors;
mapping(uint256 => Permission) public tokenPermissions;
// Mapping to store global permissions
mapping(address => GlobalPermission) public globalPermissions;
string public BASE_CODE_TXID = ""; // base code txid
constructor(address _dungeonAddress, address _dungeonRewardsAddress) {
dungeonAddress = _dungeonAddress;
dungeonRewardsAddress = _dungeonRewardsAddress;
}
modifier onlyOwnerOrStaker(uint256 tokenId) {
IDungeon dungeon = IDungeon(dungeonAddress);
address owner = dungeon.ownerOf(tokenId);
bool isStaker = owner == dungeonRewardsAddress &&
isTokenStakedByAddress(tokenId, msg.sender);
require(
owner == msg.sender || isStaker,
"Caller is not owner or staker"
);
_;
}
function isTokenStakedByAddress(
uint256 tokenId,
address addressToCheck
) private view returns (bool) {
IDungeonRewards rewards = IDungeonRewards(dungeonRewardsAddress);
(uint256[] memory stakedDungeons, ) = rewards.getStakedTokens(
addressToCheck
);
for (uint i = 0; i < stakedDungeons.length; i++) {
if (stakedDungeons[i] == tokenId) {
return true;
}
}
return false;
}
modifier isPermitted(uint256 tokenId) {
IDungeon dungeon = IDungeon(dungeonAddress);
address owner = dungeon.ownerOf(tokenId);
// First, check if msg.sender is the owner. If true, no further checks needed.
if (
owner == msg.sender ||
(owner == dungeonRewardsAddress &&
isTokenStakedByAddress(tokenId, msg.sender))
) {
_;
return;
}
// Global Permissions Check
GlobalPermission memory globalPerm = globalPermissions[msg.sender];
if (globalPerm.grantedTo == msg.sender) {
bool isGlobalGrantorOwnerOrStaker = globalPerm.grantedBy == owner ||
(owner == dungeonRewardsAddress &&
isTokenStakedByAddress(tokenId, globalPerm.grantedBy));
if (isGlobalGrantorOwnerOrStaker) {
_;
return;
}
}
// Only proceed to check permissions if the sender is not the owner or a staker.
Permission memory permission = tokenPermissions[tokenId];
bool hasValidPermission = permission.grantedTo == msg.sender &&
(permission.grantedBy == owner ||
(owner == dungeonRewardsAddress &&
isTokenStakedByAddress(tokenId, permission.grantedBy)));
require(hasValidPermission, "Not authorized");
_;
}
function writeDungeonConfig(
uint256 tokenId,
DungeonData memory configData
) public isPermitted(tokenId) {
DungeonData storage dungeon = dungeons[tokenId];
dungeon.locked = configData.locked;
dungeon.trialTimeout = configData.trialTimeout;
dungeon.randomizeLayout = configData.randomizeLayout;
dungeon.passwordHash = configData.passwordHash;
dungeon.ownersMessage = configData.ownersMessage;
dungeon.gameMode = configData.gameMode;
dungeon.tilesetOverride = configData.tilesetOverride;
}
// same as above, but without tilesetOverride - costs about half the gas
function writeDungeonConfig(
uint256 tokenId,
bool locked,
uint256 trialTimeout,
bool randomizeLayout,
string memory passwordHash,
string memory ownersMessage,
string memory gameMode
) public isPermitted(tokenId) {
DungeonData storage dungeon = dungeons[tokenId];
dungeon.locked = locked;
dungeon.trialTimeout = trialTimeout;
dungeon.randomizeLayout = randomizeLayout;
dungeon.passwordHash = passwordHash;
dungeon.ownersMessage = ownersMessage;
dungeon.gameMode = gameMode;
}
function readDungeonConfig(
uint256 tokenId
)
public
view
returns (
bool locked,
uint256 trialTimeout,
bool randomizeLayout,
string memory passwordHash,
string memory ownersMessage,
string memory gameMode,
string memory tilesetOverride,
string memory codeTxId
)
{
DungeonData memory dungeon = dungeons[tokenId];
// could directly assing the fields from the dungeon struct to construct the return values as per the
// 2nd version of this function, but it's just as gas efficient to wrap it in a return like this
return (
dungeon.locked,
dungeon.trialTimeout,
dungeon.randomizeLayout,
dungeon.passwordHash,
dungeon.ownersMessage,
dungeon.gameMode,
dungeon.tilesetOverride,
BASE_CODE_TXID
);
}
// read Dungeon Config with custom floors
function readDungeonConfig(
uint256 tokenId, uint256 maxFloorNumber
)
public
view
returns (
bool locked,
uint256 trialTimeout,
bool randomizeLayout,
string memory passwordHash,
string memory ownersMessage,
string memory gameMode,
string memory tilesetOverride,
string memory codeTxId,
uint256[] memory customFloors
)
{
DungeonData memory dungeon = dungeons[tokenId];
uint256 currentVersion = tokenVersions[tokenId];
// Directly use the fields from the dungeon struct to construct the return values
locked = dungeon.locked;
trialTimeout = dungeon.trialTimeout;
randomizeLayout = dungeon.randomizeLayout;
passwordHash = dungeon.passwordHash;
ownersMessage = dungeon.ownersMessage;
gameMode = dungeon.gameMode;
tilesetOverride = dungeon.tilesetOverride;
codeTxId = BASE_CODE_TXID;
// Use the helper function to get customFloors - do it this way to avoid CompilerError: Stack too deep
customFloors = getCustomFloors(tokenId, maxFloorNumber, currentVersion);
}
// collect customFloors data
function getCustomFloors(uint256 tokenId, uint256 maxFloorNumber, uint256 currentVersion)
internal
view
returns (uint256[] memory customFloors)
{
uint256[] memory tempFloors = new uint256[](maxFloorNumber);
uint256 count = 0;
for (uint256 floor = 0; floor <= maxFloorNumber; floor++) {
if (dungeonFloors[tokenId][floor].version == currentVersion &&
bytes(dungeonFloors[tokenId][floor].txId).length > 0) {
tempFloors[count] = floor;
count++;
}
}
customFloors = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
customFloors[i] = tempFloors[i];
}
}
function writeFloorData(
uint256 tokenId,
uint256 floorNumber,
string memory txId
) public isPermitted(tokenId) {
// Store the transaction ID for the specific floor
dungeonFloors[tokenId][floorNumber] = FloorData(txId, tokenVersions[tokenId]);
}
function readFloorData(
uint256 tokenId,
uint256 floorNumber
) public view returns (string memory) {
FloorData memory floorData = dungeonFloors[tokenId][floorNumber];
// Return the transaction ID for the specific floor
if(floorData.version == tokenVersions[tokenId]) {
return floorData.txId;
} else {
return ""; // Indicates no data for the current version
}
}
function resetAllFloors(uint256 tokenId) public isPermitted(tokenId) {
tokenVersions[tokenId] += 1; // Increment the version to "reset" the data
}
function assignPermission(
uint256 tokenId,
address to
) public onlyOwnerOrStaker(tokenId) {
tokenPermissions[tokenId] = Permission(msg.sender, to);
emit AssignPerms(msg.sender, to, tokenId);
}
// Assign global permission
function assignGlobalPermission(address to) public {
globalPermissions[to] = GlobalPermission({
grantedTo: to,
grantedBy: msg.sender
});
emit AssignGlobalPerms(msg.sender, to);
}
// Optional: Function to revoke global permission
function revokeGlobalPermission(address to) public {
require(
globalPermissions[to].grantedBy == msg.sender,
"Not authorized to revoke"
);
delete globalPermissions[to];
emit RevokeGlobalPerms(msg.sender, to);
}
// set the ethscription txid of the base codebase
function setBaseCodeTxid(string memory baseCodeTxid) public onlyOwner {
BASE_CODE_TXID = baseCodeTxid;
}
// read the ethscription txid of the base codebase
function getBaseCodeTxid() public view returns (string memory) {
return BASE_CODE_TXID;
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.10;
error NotOwner();
// https://github.com/m1guelpf/erc721-drop/blob/main/src/LilOwnable.sol
abstract contract Ownable {
address internal _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
modifier onlyOwner() {
require(_owner == msg.sender);
_;
}
constructor() {
_owner = msg.sender;
}
function owner() external view returns (address) {
return _owner;
}
function transferOwnership(address _newOwner) external {
if (msg.sender != _owner) revert NotOwner();
_owner = _newOwner;
}
function renounceOwnership() public {
if (msg.sender != _owner) revert NotOwner();
_owner = address(0);
}
function supportsInterface(bytes4 interfaceId)
public
pure
virtual
returns (bool)
{
return interfaceId == 0x7f5828d0; // ERC165 Interface ID for ERC173
}
}
{
"compilationTarget": {
"DungeonConfig.sol": "DungeonConfig"
},
"evmVersion": "shanghai",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_dungeonAddress","type":"address"},{"internalType":"address","name":"_dungeonRewardsAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"NotOwner","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"AssignGlobalPerms","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"AssignPerms","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"RevokeGlobalPerms","type":"event"},{"inputs":[],"name":"BASE_CODE_TXID","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"assignGlobalPermission","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"to","type":"address"}],"name":"assignPermission","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"dungeonAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"dungeonFloors","outputs":[{"internalType":"string","name":"txId","type":"string"},{"internalType":"uint256","name":"version","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dungeonRewardsAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"dungeons","outputs":[{"internalType":"bool","name":"locked","type":"bool"},{"internalType":"uint256","name":"trialTimeout","type":"uint256"},{"internalType":"bool","name":"randomizeLayout","type":"bool"},{"internalType":"string","name":"passwordHash","type":"string"},{"internalType":"string","name":"ownersMessage","type":"string"},{"internalType":"string","name":"gameMode","type":"string"},{"internalType":"string","name":"tilesetOverride","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBaseCodeTxid","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"globalPermissions","outputs":[{"internalType":"address","name":"grantedTo","type":"address"},{"internalType":"address","name":"grantedBy","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"readDungeonConfig","outputs":[{"internalType":"bool","name":"locked","type":"bool"},{"internalType":"uint256","name":"trialTimeout","type":"uint256"},{"internalType":"bool","name":"randomizeLayout","type":"bool"},{"internalType":"string","name":"passwordHash","type":"string"},{"internalType":"string","name":"ownersMessage","type":"string"},{"internalType":"string","name":"gameMode","type":"string"},{"internalType":"string","name":"tilesetOverride","type":"string"},{"internalType":"string","name":"codeTxId","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"maxFloorNumber","type":"uint256"}],"name":"readDungeonConfig","outputs":[{"internalType":"bool","name":"locked","type":"bool"},{"internalType":"uint256","name":"trialTimeout","type":"uint256"},{"internalType":"bool","name":"randomizeLayout","type":"bool"},{"internalType":"string","name":"passwordHash","type":"string"},{"internalType":"string","name":"ownersMessage","type":"string"},{"internalType":"string","name":"gameMode","type":"string"},{"internalType":"string","name":"tilesetOverride","type":"string"},{"internalType":"string","name":"codeTxId","type":"string"},{"internalType":"uint256[]","name":"customFloors","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"floorNumber","type":"uint256"}],"name":"readFloorData","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"resetAllFloors","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"revokeGlobalPermission","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"baseCodeTxid","type":"string"}],"name":"setBaseCodeTxid","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"tokenPermissions","outputs":[{"internalType":"address","name":"grantedBy","type":"address"},{"internalType":"address","name":"grantedTo","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bool","name":"locked","type":"bool"},{"internalType":"uint256","name":"trialTimeout","type":"uint256"},{"internalType":"bool","name":"randomizeLayout","type":"bool"},{"internalType":"string","name":"passwordHash","type":"string"},{"internalType":"string","name":"ownersMessage","type":"string"},{"internalType":"string","name":"gameMode","type":"string"}],"name":"writeDungeonConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"components":[{"internalType":"bool","name":"locked","type":"bool"},{"internalType":"uint256","name":"trialTimeout","type":"uint256"},{"internalType":"bool","name":"randomizeLayout","type":"bool"},{"internalType":"string","name":"passwordHash","type":"string"},{"internalType":"string","name":"ownersMessage","type":"string"},{"internalType":"string","name":"gameMode","type":"string"},{"internalType":"string","name":"tilesetOverride","type":"string"}],"internalType":"struct DungeonConfig.DungeonData","name":"configData","type":"tuple"}],"name":"writeDungeonConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"floorNumber","type":"uint256"},{"internalType":"string","name":"txId","type":"string"}],"name":"writeFloorData","outputs":[],"stateMutability":"nonpayable","type":"function"}]