Overview
S Balance
S Value
$0.00More Info
Private Name Tags
ContractCreator
Loading...
Loading
This contract may be a proxy contract. Click on More Options and select Is this a proxy? to confirm and enable the "Read as Proxy" & "Write as Proxy" tabs.
Contract Source Code Verified (Exact Match)
Contract Name:
PvpController
Compiler Version
v0.8.23+commit.f704f362
Optimization Enabled:
Yes with 50 runs
Other Settings:
istanbul EvmVersion
Contract Source Code (Solidity Standard Json-Input format)
// SPDX-License-Identifier: BUSL-1.1 /** ▒▓▒ ▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓▓▒ ▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓██▓ ▒██▒▓▓▓▓█▓██████████████████▓ ▒▒▒▓███████████████▒ ▒██▒▓█████████████████████▒ ▒▓██████████▓███████ ▒███████████▓▒ ▒███▓▓██████▓ █████████▒ ▒▓▒▓███████▒ ███████▓ ▒▒▒▒▒▓▓█▓▒ ▓█▓████████ ▒▒▒▒▒ ▒▒▒▒▓▓▓█████▒ ▓█████████▓ ▒▓▓▓▒▓██████▓ ▒▓▓████████▒ ▒██▓▓▓███████▒ ▒▒▓███▓████ ▒███▓█████▒ ▒▒█████▓██▓ ██████▓ ▒▒▒▓██▓██▓█████▒ ▒▒▓▓▒ ▒██▓▒▓▓████████ ▓█████▓███████▓ ██▓▓██████████▒ ▒█████████████ ███████████▓ ▒▓▓▓▓▓▓▒▓ ▒█████████▒ ▒▓▓ ▒▓█▒ ▒▒█▒▒ ▓██████ ▒▒▓▓▒ ▒▒█▒ ▓▒ ▒████ ▒▓█▓█▓▒ ▓▒██▓▒ ██ ▒▓█▓▓▓██▒ ▓█▓▓▓▓▓█▓▓▓▒ ▒▒▒ ▒▒▒▓▓▓▓▒▓▒▒▓▒▓▓▓▓▓▓▓▓▒ ▒▓█▒ ▒▓▒▓█▓ ▒▓█▓▓▓▓▓▓▓▓▓▓▒ ▒▒▒▓▒ ▒▒▒▓▓ ▓▓ ▓▓█▓ ▒▒▓▓ ▒▒█▒ ▒▓▒▓█▓ ▒▒▓▓▓▒▓▒ ▒▓▓▓▒█▒ ▒▒▒█▒ ▒▒█▓▒▒▒▓▓▓▒ ▓██▓▓▓▓▓▓▓███▓ ▒ ▒▓▓█▓ ▒▓▓▓▓█▓█▓ ▒█▓▓▒ ▓▓█▓▒▓█▓▒▒ ▓█▓ ▓███▓ ▓▓▒ ▒▒▓▓█▓▒▒▓█▒ ▒▓██▓ ▓██▓▒ ▒█▓ ▓▓██ ▒▓▓▓▒▒▓█▓ ▒▓████▒ ██▓▓▒▒▒▒▓▓███▓▒ ▒▓▓▓▓▒▒ ▒▓▓▓▓▓▓▓▒▒▒▓█▓▓▓▓█▓▓▒▒▓▓▓▓▓▒ ▒▓████▓▒ ▓▓███████▓▓▒ */ pragma solidity 0.8.23; import "../interfaces/IApplicationEvents.sol"; import "../interfaces/IPvpController.sol"; import "../lib/PvpControllerLib.sol"; import "../lib/PvpFightLib.sol"; import "../proxy/Controllable.sol"; import "../relay/ERC2771Context.sol"; import "../openzeppelin/ERC721Holder.sol"; contract PvpController is Initializable, Controllable, ERC2771Context, IPvpController, ERC721Holder { //region ------------------------ Constants /// @notice Version of the contract /// @dev Should be incremented when contract changed string public constant override VERSION = "1.0.0"; //endregion ------------------------ Constants //region ------------------------ Initializer function init(address controller_) external initializer { __Controllable_init(controller_); PvpControllerLib._S().pvpParam[IPvpController.PvpParams.MIN_HERO_LEVEL_1] = PvpControllerLib.DEFAULT_MIN_HERO_LEVEL; } //endregion ------------------------ Initializer //region ------------------------ View function getBiomeOwner(uint8 biome) external view returns (uint guildId) { return PvpControllerLib.getBiomeOwner(biome); } function getStartedEpoch(uint8 biome) external view returns (uint32 epochWeek) { return PvpControllerLib.getStartedEpoch(biome); } function getDominationCounter(uint8 biome) external view returns (uint16 dominationCounter) { return PvpControllerLib.getDominationCounter(biome); } /// @notice List of guilds that send domination request for the biome function getBiomeGuilds(uint8 biome, uint32 week) external view returns (uint[] memory guildIds) { return PvpControllerLib.getBiomeGuilds(biome, week); } /// @return biome Biome where the guild is going to dominate in the given epoch function getDominationRequest(uint guildId, uint32 week) external view returns (uint8 biome) { return PvpControllerLib.getDominationRequest(guildId, week); } function getGuildPoints(uint8 biome, uint32 epochWeek, uint guildId) external view returns (uint) { return PvpControllerLib.getGuildPoints(biome, epochWeek, guildId); } function getFreeUsers(uint8 biome, uint32 epochWeek, uint guildId) external view returns (address[] memory) { return PvpControllerLib.getFreeUsers(biome, epochWeek, guildId); } function getPvpStrategy(uint8 biome, uint32 epochWeek, address hero, uint heroId) external view returns (bytes memory) { return PvpControllerLib.getPvpStrategy(biome, epochWeek, hero, heroId); } function getPvpStrategyKind(uint8 biome, uint32 epochWeek, address hero, uint heroId) external view returns (uint) { return PvpControllerLib.getPvpStrategyKind(biome, epochWeek, hero, heroId); } /// @notice Number of pvp-fights registered for the user in the given epoch function getFightDataLength(uint32 epochWeek, address user) external view returns (uint) { return PvpControllerLib.getFightDataLength(epochWeek, user); } function getFightDataByIndex(uint32 epochWeek, address user, uint index0) external view returns (IPvpController.PvpFightData memory) { return PvpControllerLib.getFightDataByIndex(epochWeek, user, index0); } /// @notice List of the users registered for pvp-fight in the given week and biome function registeredUsers(uint8 biome, uint32 epochWeek, uint guildId) external view returns (address[] memory) { return PvpControllerLib.registeredUsers(biome, epochWeek, guildId); } /// @notice Hero registered by the user for pvp-fight in the given week and biome function registeredHero(uint8 biome, uint32 epochWeek, uint guildId, address user) external view returns (address hero, uint heroId) { return PvpControllerLib.registeredHero(biome, epochWeek, guildId, user); } /// @notice Biome owned currently by the given guild function ownedBiome(uint guildId) external view returns (uint8 biome) { return PvpControllerLib.ownedBiome(guildId); } /// @notice Get week for the given timestamp. Assume that first day of the week is Monday function getCurrentEpochWeek(uint blockTimestamp) external pure returns (uint32) { return PvpControllerLib.getCurrentEpochWeek(blockTimestamp); } function currentWeek() external view returns (uint32) { return PvpControllerLib.getCurrentEpochWeek(block.timestamp); } /// @notice Get biome tax /// @return guildId Owner of the biome /// @return taxPercent Final tax percent, [0...100_000], decimals 3 function getBiomeTax(uint8 biome) external view returns (uint guildId, uint taxPercent) { return PvpControllerLib.getBiomeTax(biome); } /// @notice Check if the user has a pvp-hero registered for pvp-fight in the given epoch function hasPvpHero(address user, uint guildId, uint32 week) external view returns (bool) { return PvpControllerLib.hasPvpHero(user, guildId, week); } /// @notice Check if the given hero is staked in pvp controller in the given epoch function isHeroStaked(address hero, uint heroId, uint32 epoch) external view returns (bool staked) { return PvpControllerLib.isHeroStaked(hero, heroId, epoch); } /// @notice Check if the given hero is staked in pvp controller in the current epoch function isHeroStakedCurrently(address hero, uint heroId) external view returns (bool staked) { return PvpControllerLib.isHeroStaked(hero, heroId, PvpControllerLib.getCurrentEpochWeek(block.timestamp)); } function getUserState(uint32 week, address user) external view returns (IPvpController.PvpUserState memory userState) { return PvpControllerLib.getUserState(week, user); } /// @notice Get min hero level allowed for pvp-fight function getMinHeroLevel() external view returns (uint) { return PvpControllerLib.getMinHeroLevel(); } function getCounterFightId() external view returns (uint48) { return PvpControllerLib.getCounterFightId(); } //endregion ------------------------ View //region ------------------------ Deployer actions function setMinHeroLevel(uint level) external { PvpControllerLib.setMinHeroLevel(IController(controller()), level); } function setGuildStakingAdapter(address adapter_) external { PvpControllerLib.setGuildStakingAdapter(IController(controller()), adapter_); } //endregion ------------------------ Deployer actions //region ------------------------ Domination actions /// @notice Select new domination target once per epoch function selectBiomeForDomination(uint8 biome) external { return PvpControllerLib.selectBiomeForDomination( _msgSender(), IController(controller()), biome, block.timestamp, CalcLib.pseudoRandom ); } /// @notice Withdraw hero from pvp for the current epoch function removePvpHero() external { PvpControllerLib.removePvpHero(_msgSender(), IController(controller()), block.timestamp); } /// @notice Stake hero for pvp for the current epoch. /// User is able to register a hero only once per epoch, the hero cannot be replaced, only removed. /// @param pvpStrategy abi.encode(PvpAttackInfoDefaultStrategy) function addPvpHero(address hero, uint heroId, bytes memory pvpStrategy, uint8 maxFights) external { PvpControllerLib.addPvpHero( _msgSender(), IController(controller()), hero, heroId, pvpStrategy, maxFights, block.timestamp, CalcLib.pseudoRandom ); } /// @notice Change epoch if the current epoch is completed, update biome owner function updateEpoch(uint8 biome) external { PvpControllerLib.updateEpoch(biome, block.timestamp, CalcLib.pseudoRandom); } /// @notice Update epoch if necessary and return biome owner and biome tax /// @return guildId Owner of the biome /// @return taxPercent Tax percent [0...100_000], decimals 3 function refreshBiomeTax(uint8 biome) external returns (uint guildId, uint taxPercent) { return PvpControllerLib.refreshBiomeTax(biome, block.timestamp, CalcLib.pseudoRandom); } function onGuildDeletion(uint guildId) external { PvpControllerLib.onGuildDeletion(IController(controller()), guildId); } //endregion ------------------------ Domination actions //region ------------------------ PvP actions function prepareFight() external { PvpFightLib.prepareFight(_msgSender(), IController(controller()), block.timestamp, CalcLib.pseudoRandom); } function startFight(uint8 maxCountTurns) external { PvpFightLib.startFight(_msgSender(), IController(controller()), block.timestamp, maxCountTurns); } //endregion ------------------------ PvP actions }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; /// @notice All errors of the app interface IAppErrors { //region ERC20Errors /** * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. * @param sender Address whose tokens are being transferred. * @param balance Current balance for the interacting account. * @param needed Minimum amount required to perform a transfer. */ error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); /** * @dev Indicates a failure with the token `sender`. Used in transfers. * @param sender Address whose tokens are being transferred. */ error ERC20InvalidSender(address sender); /** * @dev Indicates a failure with the token `receiver`. Used in transfers. * @param receiver Address to which tokens are being transferred. */ error ERC20InvalidReceiver(address receiver); /** * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. * @param spender Address that may be allowed to operate on tokens without being their owner. * @param allowance Amount of tokens a `spender` is allowed to operate with. * @param needed Minimum amount required to perform a transfer. */ error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); /** * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. * @param approver Address initiating an approval operation. */ error ERC20InvalidApprover(address approver); /** * @dev Indicates a failure with the `spender` to be approved. Used in approvals. * @param spender Address that may be allowed to operate on tokens without being their owner. */ error ERC20InvalidSpender(address spender); //endregion ERC20Errors //region ERC721Errors /** * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20. * Used in balance queries. * @param owner Address of the current owner of a token. */ error ERC721InvalidOwner(address owner); /** * @dev Indicates a `tokenId` whose `owner` is the zero address. * @param tokenId Identifier number of a token. */ error ERC721NonexistentToken(uint256 tokenId); /** * @dev Indicates an error related to the ownership over a particular token. Used in transfers. * @param sender Address whose tokens are being transferred. * @param tokenId Identifier number of a token. * @param owner Address of the current owner of a token. */ error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); /** * @dev Indicates a failure with the token `sender`. Used in transfers. * @param sender Address whose tokens are being transferred. */ error ERC721InvalidSender(address sender); /** * @dev Indicates a failure with the token `receiver`. Used in transfers. * @param receiver Address to which tokens are being transferred. */ error ERC721InvalidReceiver(address receiver); /** * @dev Indicates a failure with the `operator`’s approval. Used in transfers. * @param operator Address that may be allowed to operate on tokens without being their owner. * @param tokenId Identifier number of a token. */ error ERC721InsufficientApproval(address operator, uint256 tokenId); /** * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. * @param approver Address initiating an approval operation. */ error ERC721InvalidApprover(address approver); /** * @dev Indicates a failure with the `operator` to be approved. Used in approvals. * @param operator Address that may be allowed to operate on tokens without being their owner. */ error ERC721InvalidOperator(address operator); //endregion ERC721Errors error ZeroAddress(); error ZeroValueNotAllowed(); error ZeroToken(); error LengthsMismatch(); error NotEnoughBalance(); error NotEnoughAllowance(); error EmptyNameNotAllowed(); error NotInitialized(); error AlreadyInitialized(); error ReentrancyGuardReentrantCall(); error TooLongString(); error AlreadyDeployed(address deployed); error AlreadyClaimed(); //region Restrictions error ErrorNotDeployer(address sender); error ErrorNotGoc(); error NotGovernance(address sender); error ErrorOnlyEoa(); error NotEOA(address sender); error ErrorForbidden(address sender); error AdminOnly(); error ErrorNotItemController(address sender); error ErrorNotHeroController(address sender); error ErrorNotDungeonFactory(address sender); error ErrorNotObjectController(address sender); error ErrorNotStoryController(); error ErrorNotAllowedSender(); error MintNotAllowed(); error NotPvpController(); //endregion Restrictions //region PackingLib error TooHighValue(uint value); error IntValueOutOfRange(int value); error OutOfBounds(uint index, uint length); error UnexpectedValue(uint expected, uint actual); error WrongValue(uint newValue, uint actual); error IntOutOfRange(int value); error ZeroValue(); /// @notice packCustomDataChange requires an input string with two zero bytes at the beginning /// 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0000 /// This error happens if these bytes are not zero error IncompatibleInputString(); error IncorrectOtherItemTypeKind(uint8 kind); //endregion PackingLib //region Hero error ErrorHeroIsNotRegistered(address heroToken); error ErrorHeroIsDead(address heroToken, uint heroTokenId); error ErrorHeroNotInDungeon(); error HeroInDungeon(); error ErrorNotOwner(address token, uint tokenId); error Staked(address heroToken, uint heroId); error NameTaken(); error TooBigName(); error WrongSymbolsInTheName(); error NoPayToken(address token, uint payTokenAmount); error AlreadyHaveReinforcement(); /// @notice SIP-001 - Reinforcement requires 3 skills error ErrorReinforcementRequiresThreeSkills(); error WrongTier(uint tier); error NotEnoughNgLevel(uint8 ngLevel); error NgpNotActive(address hero); error RebornNotAllowed(); error AlreadyPrePaidHero(); error SandboxTierForbidden(); error SandboxPrepaidOnly(); error SandboxNgZeroOnly(); error SandboxModeNotAllowed(); error SandboxUpgradeModeRequired(); error SandboxModeRequired(); error SandboxItemOutside(); error SandboxItemNotActive(); error SandboxItemNotRegistered(); error SandboxItemAlreadyEquipped(); error SandboxDifferentHeroesNotAllowed(); error HeroWasTransferredBetweenAccounts(); //endregion Hero //region Dungeon error ErrorDungeonIsFreeAlready(); error ErrorNoEligibleDungeons(); error ErrorDungeonBusy(); error ErrorNoDungeonsForBiome(uint8 heroBiome); error ErrorDungeonCompleted(); error ErrorAlreadyInDungeon(); error NotEnoughTokens(uint balance, uint expectedBalance); error DungeonAlreadySpecific(uint16 dungNum); error DungeonAlreadySpecific2(uint16 dungNum); error WrongSpecificDungeon(); //endregion Dungeon //region Items error ErrorItemNotEligibleForTheSlot(uint itemType, uint8 itemSlot); error ErrorItemSlotBusyHand(uint8 slot); error ErrorItemSlotBusy(); error ErrorItemNotInSlot(); error ErrorConsumableItemIsUsed(address item); error ErrorCannotRemoveItemFromMap(); error ErrorCannotRemoveDataFromMap(); error EquippedItemsExist(); error ItemEquipped(address item, uint itemId); error ZeroItemMetaType(); error NotZeroOtherItemMetaType(); error ZeroLevel(); error ItemTypeChanged(); error ItemMetaTypeChanged(); error UnknownItem(address item); error ErrorEquipForbidden(); error EquipForbiddenInDungeon(); error TakeOffForbiddenInDungeon(); error Consumable(address item); error NotConsumable(address item); error Broken(address item); error ZeroLife(); error RequirementsToItemAttributes(); error NotEquipped(address item); error ZeroDurability(); error ZeroAugmentation(); error TooHighAgLevel(uint8 augmentationLevel); error UseForbiddenZeroPayToken(); error IncorrectMinMaxAttributeRange(int32 min, int32 max); error SameIdsNotAllowed(); error ZeroFragility(); error OtherTypeItemNotRepairable(); error NotOther(); error DoubleItemUsageForbidden(uint itemIndex, address[] items); error ItemAlreadyUsedInSlot(address item, uint8 equippedSlot); error WrongWayToRegisterItem(); error UnionItemNotFound(address item); error WrongListUnionItemTokens(address item, uint countTokens, uint requiredCountTokens); error UnknownUnionConfig(uint unionConfigId); error UserHasNoKeyPass(address user, address keyPassItem); error MaxValue(uint value); error UnexpectedOtherItem(address item); error NotExist(); error ItemNotFound(address item, uint itemId); //endregion Items //region Stages error ErrorWrongStage(uint stage); error ErrorNotStages(); //endregion Stages //region Level error ErrorWrongLevel(uint heroLevel); error ErrorLevelTooLow(uint heroLevel); error ErrorHeroLevelStartFrom1(); error ErrorWrongLevelUpSum(); error ErrorMaxLevel(); //endregion Level //region Treasure error ErrorNotValidTreasureToken(address treasureToken); //endregion Treasure //region State error ErrorPaused(); error ErrorNotReady(); error ErrorNotObject1(); error ErrorNotObject2(); error ErrorNotCompleted(); //endregion State //region Biome error ErrorNotBiome(); error ErrorIncorrectBiome(uint biome); error TooHighBiome(uint biome); //endregion Biome //region Misc error ErrorWrongMultiplier(uint multiplier); error ErrorNotEnoughMana(uint32 mana, uint requiredMana); error ErrorExperienceMustNotDecrease(); error ErrorNotEnoughExperience(); error ErrorNotChances(); error ErrorNotEligible(address heroToken, uint16 dungNum); error ErrorZeroKarmaNotAllowed(); //endregion Misc //region GOC error GenObjectIdBiomeOverflow(uint8 biome); error GenObjectIdSubTypeOverflow(uint subType); error GenObjectIdIdOverflow(uint id); error UnknownObjectTypeGoc1(uint8 objectType); error UnknownObjectTypeGoc2(uint8 objectType); error UnknownObjectTypeGocLib1(uint8 objectType); error UnknownObjectTypeGocLib2(uint8 objectType); error UnknownObjectTypeForSubtype(uint8 objectSubType); error FightDelay(); error ZeroChance(); error TooHighChance(uint32 chance); error TooHighRandom(uint random); error EmptyObjects(); error ObjectNotFound(); error WrongGetObjectTypeInput(); error WrongChances(uint32 chances, uint32 maxChances); //endregion GOC //region Story error PageNotRemovedError(uint pageId); error NotItem1(); error NotItem2(); error NotRandom(uint32 random); error NotHeroData(); error NotGlobalData(); error ZeroStoryIdRemoveStory(); error ZeroStoryIdStoryAction(); error ZeroStoryIdAction(); error NotEnoughAmount(uint balance, uint requiredAmount); error NotAnswer(); error AnswerStoryIdMismatch(uint16 storyId, uint16 storyIdFromAnswerHash); error AnswerPageIdMismatch(uint16 pageId, uint16 pageIdFromAnswerHash); error NotSkippableStory(); error StoryNotPassed(); error SkippingNotAllowed(); //endregion Story //region FightLib error NotMagic(); error NotAType(uint atype); //endregion FightLib //region MonsterLib error NotYourDebuffItem(); error UnknownAttackType(uint attackType); error NotYourAttackItem(); /// @notice The skill item cannot be used because it doesn't belong either to the hero or to the hero's helper error NotYourBuffItem(); //endregion MonsterLib //region GameToken error ApproveToZeroAddress(); error MintToZeroAddress(); error TransferToZeroAddress(); error TransferAmountExceedsBalance(uint balance, uint value); error InsufficientAllowance(); error BurnAmountExceedsBalance(); error NotMinter(address sender); //endregion GameToken //region NFT error TokenTransferNotAllowed(); error IdOverflow(uint id); error NotExistToken(uint tokenId); error EquippedItemIsNotAllowedToTransfer(uint tokenId); //endregion NFT //region CalcLib error TooLowX(uint x); //endregion CalcLib //region Controller error NotFutureGovernance(address sender); //endregion Controller //region Oracle error OracleWrongInput(); //endregion Oracle //region ReinforcementController error AlreadyStaked(); error MaxFee(uint8 fee); error MinFee(uint8 fee); error StakeHeroNotStats(); error NotStaked(); error NoStakedHeroes(); error GuildHelperNotAvailable(uint guildId, address helper, uint helperId); error PvpStaked(); error HelperNotAvailableInGivenBiome(); //endregion ReinforcementController //region SponsoredHero error InvalidHeroClass(); error ZeroAmount(); error InvalidProof(); error NoHeroesAvailable(); error AlreadyRegistered(); //endregion SponsoredHero //region SacraRelay error SacraRelayNotOwner(); error SacraRelayNotDelegator(); error SacraRelayNotOperator(); error SacraRelayInvalidChainId(uint callChainId, uint blockChainId); error SacraRelayInvalidNonce(uint callNonce, uint txNonce); error SacraRelayDeadline(); error SacraRelayDelegationExpired(); error SacraRelayNotAllowed(); error SacraRelayInvalidSignature(); /// @notice This error is generated when custom error is caught /// There is no info about custom error in SacraRelay /// but you can decode custom error by selector, see tests error SacraRelayNoErrorSelector(bytes4 selector, string tracingInfo); /// @notice This error is generated when custom error is caught /// There is no info about custom error in SacraRelay /// but you can decode custom error manually from {errorBytes} as following: /// if (keccak256(abi.encodeWithSignature("MyError()")) == keccak256(errorBytes)) { ... } error SacraRelayUnexpectedReturnData(bytes errorBytes, string tracingInfo); error SacraRelayCallToNotContract(address notContract, string tracingInfo); //endregion SacraRelay //region Misc error UnknownHeroClass(uint heroClass); error AbsDiff(int32 a, int32 b); //region Misc //region ------------------------ UserController error NoAvailableLootBox(address msgSender, uint lootBoxKind); error FameHallHeroAlreadyRegistered(uint8 openedNgLevel); //endregion ------------------------ UserController //region ------------------------ Guilds error AlreadyGuildMember(); error NotGuildMember(); error WrongGuild(); error GuildActionForbidden(uint right); error GuildHasMaxSize(uint guildSize); error GuildHasMaxLevel(uint level); error TooLongUrl(); error TooLongDescription(); error CannotRemoveGuildOwnerFromNotEmptyGuild(); error GuildControllerOnly(); error GuildAlreadyHasShelter(); error ShelterIsBusy(); error ShelterIsNotRegistered(); error ShelterIsNotOwnedByTheGuild(); error ShelterIsInUse(); error GuildHasNoShelter(); error ShelterBidIsNotAllowedToBeUsed(); error ShelterHasHeroesInside(); error SecondGuildAdminIsNotAllowed(); error NotEnoughGuildBankBalance(uint guildId); error GuildReinforcementCooldownPeriod(); error NoStakedGuildHeroes(); error NotStakedInGuild(); error ShelterHasNotEnoughLevelForReinforcement(); error NotBusyGuildHelper(); error TooLowGuildLevel(); /// @notice Target biome can be selected only once per epoch error BiomeAlreadySelected(); error NoDominationRequest(); error PvpFightIsNotPrepared(uint8 biome, uint32 week, address user); error PvpFightIsCompleted(uint8 biome, uint32 week, address user); error TooLowMaxCountTurns(); error UserTokensVaultAlreadySet(); error DifferentBiomeInPvpFight(); error PvpFightOpponentNotFound(); error PvpHeroHasInitializedFight(); error PvpHeroNotRegistered(); /// @notice User should unregister pvp-hero from prev biome and only then register it in the new biome error UserHasRegisteredPvpHeroInBiome(uint8 biome); error UserHasRegisteredPvpHero(); error UserNotAllowedForPvpInCurrentEpoch(uint week); error UnknownPvpStrategy(); error GuildRequestNotActive(); error GuildRequestNotAvailable(); error NotAdminCannotAddMemberWithNotZeroRights(); //endregion ------------------------ Guilds //region ------------------------ Shelters error ErrorNotShelterController(); error ErrorNotGuildController(); error ShelterHasNotItem(uint shelterId, address item); error MaxNumberItemsSoldToday(uint numSoldItems, uint limit); error GuildHasNotEnoughPvpPoints(uint64 pointsAvailable, uint pointRequired); error FreeShelterItemsAreNotAllowed(uint shelterId, address item); error TooLowShelterLevel(uint8 shelterLevel, uint8 allowedShelterLevel); error NotEnoughPvpPointsCapacity(address user, uint usedPoints, uint pricePvpPoints, uint64 capactiy); error IncorrectShelterLevel(uint8 shelterLevel); //endregion ------------------------ Shelters //region ------------------------ Auction error WrongAuctionPosition(); error AuctionPositionClosed(); error AuctionBidOpened(uint positionId); error TooLowAmountToBid(); error AuctionEnded(); error TooLowAmountForNewBid(); error AuctionSellerOnly(); error AuctionBuyerOnly(); error AuctionBidNotFound(); error AuctionBidClosed(); error OnlyShelterAuction(); error CannotCloseLastBid(); error AuctionNotEnded(); error NotShelterAuction(); error AuctionPositionOpened(uint positionId); error AuctionSellerCannotBid(); error AuctionGuildWithShelterCannotBid(); error AuctionBidExists(); //endregion ------------------------ Auction //region ------------------------ Pawnshop error AuctionPositionNotSupported(uint positionId); error PositionNotSupported(uint positionId); error NotNftPositionNotSupported(uint positionId); error CallFailed(bytes callResultData); error PawnShopZeroOwner(); error PawnShopZeroFeeRecipient(); error PawnShopNotOwner(); error PawnShopAlreadyAnnounced(); error PawnShopTimeLock(); error PawnShopWrongAddressValue(); error PawnShopWrongUintValue(); error PawnShopZeroAddress(); error PawnShopTooHighValue(); error PawnShopZeroAToken(); error PawnShopZeroCToken(); error PawnShopWrongAmounts(); error PawnShopPosFeeForInstantDealForbidden(); error PawnShopPosFeeAbsurdlyHigh(); error PawnShopIncorrect(); error PawnShopWrongId(); error PawnShopNotBorrower(); error PawnShopPositionClosed(); error PawnShopPositionExecuted(); error PawnShopWrongBidAmount(); error PawnShopTooLowBid(); error PawnShopNewBidTooLow(); error PawnShopBidAlreadyExists(); error PawnShopAuctionEnded(); error PawnShopNotLender(); error PawnShopTooEarlyToClaim(); error PawnShopPositionNotExecuted(); error PawnShopAlreadyClaimed(); error PawnShopAuctionNotEnded(); error PawnShopBidClosed(); error PawnShopNoBids(); error PawnShopAuctionBidNotFound(); error PawnShopWrongBid(); error PawnShopBidNotFound(); //endregion ------------------------ Pawnshop }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IGOC.sol"; import "./IStatController.sol"; import "./IDungeonFactory.sol"; import "./IStoryController.sol"; import "./IFightCalculator.sol"; import "./IPvpController.sol"; /// @notice All events of the app interface IApplicationEvents { //region ------------------------ Common event SetOperator(address operator, bool remove); event Salvage(address receiver, address token, uint amount); //endregion ------------------------ Common //region ------------------ StatController event HeroItemSlotChanged( address heroToken, uint heroTokenId, uint itemType, uint itemSlot, address itemToken, uint itemTokenId, bool equip, address caller ); event CurrentStatsChanged( address heroToken, uint heroTokenId, IStatController.ChangeableStats change, bool increase, address caller ); event BonusAttributesChanged( address heroToken, uint heroTokenId, bool add, bool temporally, address caller ); event TemporallyAttributesCleared(address heroToken, uint heroTokenId, address caller); event NewHeroInited(address heroToken, uint heroTokenId, IStatController.ChangeableStats stats); event LevelUp( address heroToken, uint heroTokenId, uint heroClass, IStatController.CoreAttributes change ); event ConsumableUsed(address heroToken, uint heroTokenId, address item); event RemoveConsumableUsage(address heroToken, uint heroTokenId, address item); event HeroCustomDataChanged(address token, uint tokenId, bytes32 index, uint value); event HeroCustomDataChangedNg(address token, uint tokenId, bytes32 index, uint value, uint8 ngLevel); event HeroCustomDataCleared(address token, uint tokenId); event GlobalCustomDataChanged(bytes32 index, uint value); //endregion ------------------ StatController //region ------------------ DungeonFactoryController event DungeonLaunched( uint16 dungeonLogicNum, uint64 dungeonId, address heroToken, uint heroTokenId, address treasuryToken, uint treasuryAmount ); event BossCompleted(uint32 objectId, uint biome, address hero, uint heroId); event FreeDungeonAdded(uint8 biome, uint64 dungeonId); event ObjectOpened(uint64 dungId, address hero, uint id, uint32 objId, uint iteration, uint currentStage); event Clear(uint64 dungId); event DungeonLogicRegistered(uint16 dungLogicId, IDungeonFactory.DungeonGenerateInfo info); event DungeonLogicRemoved(uint16 dungLogicId); event DungeonSpecificLogicRegistered(uint16 dungLogicId, uint biome, uint heroCls); event DungeonSpecificLogicRemoved(uint16 dungLogicId, uint heroLvl, uint heroCls); event DungeonRegistered(uint16 dungLogicId, uint64 dungeonId); event DungeonRemoved(uint16 dungLogicId, uint64 dungeonId); event MinLevelForTreasuryChanged(address token, uint level); event ObjectAction( uint64 dungId, IGOC.ActionResult result, uint currentStage, address heroToken, uint heroTokenId, uint newStage ); /// @notice On add the item to the dungeon event AddTreasuryItem(uint64 dungId, address itemAdr, uint itemId); event AddTreasuryToken(uint64 dungId, address token, uint amount); event ClaimToken(uint64 dungId, address token, uint amount); event ClaimItem(uint64 dungId, address token, uint id); event Entered(uint64 dungId, address hero, uint id); event DungeonCompleted(uint16 dungLogicNum, uint64 dungId, address hero, uint heroId); event Exit(uint64 dungId, bool claim); event ExitForcibly(uint64 dungId, address hero, uint heroId); event FreeDungeonRemoved(uint8 biome, uint64 dungeonId); event HeroCurrentDungeonChanged(address hero, uint heroId, uint64 dungeonId); //endregion ------------------ DungeonFactoryController //region ------------------ GameObjectController event EventRegistered(uint32 objectId, IGOC.EventRegInfo eventRegInfo); event StoryRegistered(uint32 objectId, uint16 storyId); event MonsterRegistered(uint32 objectId, IGOC.MonsterGenInfo monsterGenInfo); event ObjectRemoved(uint32 objectId); event ObjectResultEvent( uint64 dungeonId, uint32 objectId, IGOC.ObjectType objectType, address hero, uint heroId, uint8 stageId, uint iteration, bytes data, IGOC.ActionResult result, uint salt ); //endregion ------------------ GameObjectController //region ------------------ StoryController event SetBurnItemsMeta(uint storyId, IStoryController.AnswerBurnRandomItemMeta meta); event SetNextObjRewriteMeta(uint storyId, IStoryController.NextObjRewriteMeta meta); event SetAnswersMeta(uint storyId, uint16[] answerPageIds, uint8[] answerHeroClasses, uint16[] answerIds); event SetAnswerNextPageMeta(uint storyId, IStoryController.AnswerNextPageMeta meta); event SetAnswerAttributeRequirements(uint storyId, IStoryController.AnswerAttributeRequirementsMeta meta); event SetAnswerItemRequirements(uint storyId, IStoryController.AnswerItemRequirementsMeta meta); event SetAnswerTokenRequirementsMeta(uint storyId, IStoryController.AnswerTokenRequirementsMeta meta); event SetAnswerAttributes(uint storyId, IStoryController.AnswerAttributesMeta meta); event SetAnswerHeroCustomDataRequirementMeta(uint storyId, IStoryController.AnswerCustomDataMeta meta); event SetAnswerGlobalCustomDataRequirementMeta(uint storyId, IStoryController.AnswerCustomDataMeta meta); event SetSuccessInfo(uint storyId, IStoryController.AnswerResultMeta meta); event SetFailInfo(uint storyId, IStoryController.AnswerResultMeta meta); event SetCustomDataResult(uint storyId, IStoryController.AnswerCustomDataResultMeta meta, IStoryController.CustomDataResult _type); event StoryCustomDataRequirements(uint storyId, bytes32 requiredCustomDataIndex, uint requiredCustomDataMinValue, uint requiredCustomDataMaxValue, bool requiredCustomDataIsHero); event StoryRequiredLevel(uint storyId, uint requiredLevel); event StoryFinalized(uint32 objectId, uint storyId); event StoryRemoved(uint32 objectId, uint storyId); event ItemBurned( address heroToken, uint heroTokenId, uint64 dungeonId, uint objectId, address nftToken, uint nftId, uint stageId, uint iteration ); /// @notice Durability of the item was reduced to 0 event ItemBroken( address heroToken, uint heroTokenId, uint64 dungeonId, uint objectId, address nftToken, uint nftId, uint stageId, uint iteration ); event NotEquippedItemBurned( address heroToken, uint heroTokenId, uint64 dungeonId, uint storyId, address nftToken, uint nftId, uint stageId, uint iteration ); event StoryChangeAttributes( uint32 objectId, address heroToken, uint heroTokenId, uint64 dungeonId, uint storyId, uint stageId, uint iteration, int32[] attributes ); //endregion ------------------ StoryController //region ------------------------ HeroController event HeroRegistered(address hero, uint8 heroClass, address payToken, uint payAmount); /// @notice Deprecated, replaced by {HeroCreatedNgpSandbox}. Don't remove - it's required by subgraph event HeroCreatedNgp(address hero, uint heroId, string name, address owner, string refCode, uint8 tier, uint8 ngLevel); event HeroCreatedNgpSandbox(address hero, uint heroId, string name, address owner, string refCode, uint8 tier, uint8 ngLevel, bool sandbox); event BiomeChanged(address hero, uint heroId, uint8 biome); event LevelUp(address hero, uint heroId, address owner, IStatController.CoreAttributes change); event ReinforcementAsked(address hero, uint heroId, address helpHeroToken, uint helpHeroId); event GuildReinforcementAsked(address hero, uint heroId, address helpHeroToken, uint helpHeroId); event OtherItemGuildReinforcement(address item, uint itemId, address hero, uint heroId, address helpHeroToken, uint helpHeroId); event ReinforcementReleased(address hero, uint heroId, address helperToken, uint helperId); event GuildReinforcementReleased(address hero, uint heroId, address helperToken, uint helperId); event Killed(address hero, uint heroId, address killer, bytes32[] dropItems, uint dropTokenAmount); event Reborn(address hero, uint heroId, uint8 newNgLevel); event BossKilled(address account, address hero, uint heroId, uint8 biome, uint8 newNgLevel, bool reborn, uint rewardAmount); event TierSetup(uint8 tier, address hero, uint72 payAmount, uint8[] slots, address[][] items); event SandboxUpgraded(address hero, uint heroId); event SandboxReturnAmountToTreasury(uint64 dungId, address token, uint amount); //endregion ------------------------ HeroController //region ------------------------ FightLib event FightResultProcessed( address sender, IFightCalculator.FightInfoInternal result, IFightCalculator.FightCall callData, uint iteration ); /// @param heroA Address of the fighter A. Address of the fighter B can be detected by fightId /// @param heroIdA ID of the figher A. ID of the fighter B can be detected by fightId event PvpFightResultProcessed( uint48 fightId, address sender, IFightCalculator.FightInfoInternal result, uint turn, address heroA, uint heroIdA ); //endregion ------------------------ FightLib //region ------------------------ Oracle event Random(uint number, uint max); //endregion ------------------------ Oracle //region ------------------------ Controller event OfferGovernance(address newGov); event GovernanceAccepted(address gov); event StatControllerChanged(address value); event StoryControllerChanged(address value); event GameObjectControllerChanged(address value); event ReinforcementControllerChanged(address value); event OracleChanged(address value); event TreasuryChanged(address value); event ItemControllerChanged(address value); event HeroControllerChanged(address value); event GameTokenChanged(address value); event DungeonFactoryChanged(address value); event ProxyUpdated(address proxy, address logic); event Claimed(address token, uint amount); event TokenStatusChanged(address token, bool status); event UserControllerChanged(address value); event GuildControllerChanged(address value); event PvpControllerChanged(address value); event GameTokenPriceChanged(uint value); event RewardsPoolChanged(address value); event ItemBoxControllerChanged(address value); event Process(address token, uint amount, address from, uint toBurn, uint toTreasury, uint toGov); //endregion ------------------------ Controller //region ------------------------ ReinforcementController event HeroStaked(address heroToken, uint heroId, uint biome, uint score); event HeroStakedV2(address heroToken, uint heroId, uint biome, uint rewardAmount); event HeroWithdraw(address heroToken, uint heroId); event HeroAsk(address heroToken, uint heroId); event HeroAskV2(address heroToken, uint heroId, uint hitsLast24h, uint fixedFee, uint helperRewardAmount); event TokenRewardRegistered(address heroToken, uint heroId, address token, uint amountAdded, uint totalAmount); event GuildTokenRewardRegistered(address heroToken, uint heroId, address token, uint amountAdded, uint guildId); event NftRewardRegistered(address heroToken, uint heroId, address token, uint id); event GuildNftRewardRegistered(address heroToken, uint heroId, address token, uint id, uint guildId); event ToHelperRatioChanged(uint value); event ClaimedToken(address heroToken, uint heroId, address token, uint amount, address recipient); event ClaimedItem(address heroToken, uint heroId, address item, uint itemId, address recipient); event MinLevelChanged(uint8 value); event MinLifeChancesChanged(uint value); //endregion ------------------------ ReinforcementController //region ------------------------ Treasury, reward pool event AssetsSentToDungeon(address dungeon, address token, uint amount); event RewardSentToUser(address receiver, address token, uint rewardAmount); event NotEnoughReward(address receiver, address token, uint rewardAmountToPay); event BaseAmountChanged(uint oldValue, uint newValue); //endregion ------------------------ Treasury, reward pool //region ------------------------ EventLib event EventResult(uint64 dungeonId, address heroToken, uint heroTokenId, uint8 stageId, IStatController.ActionInternalInfo gen, uint iteration); //endregion ------------------------ EventLib //region ------------------------ Item controller and helper contracts event ItemRegistered(address item, IItemController.RegisterItemParams info); event OtherItemRegistered(address item, IItemController.ItemMeta meta, bytes packedItemMetaData); event ItemRemoved(address item); event OtherItemRemoved(address item); event NewItemMinted(address item, uint itemId, IItemController.MintInfo info); event Equipped(address item, uint itemId, address heroToken, uint heroTokenId, uint8 itemSlot); event TakenOff(address item, uint itemId, address heroToken, uint heroTokenId, uint8 itemSlot, address destination); event ItemRepaired(address item, uint itemId, uint consumedItemId, uint16 baseDurability); event FailedToRepairItem(address item, uint itemId, uint consumedItemId, uint16 itemDurability); event Augmented(address item, uint itemId, uint consumedItemId, uint8 augLevel, IItemController.AugmentInfo info); event NotAugmented(address item, uint itemId, uint consumedItemId, uint8 augLevel); event ReduceDurability(address item, uint itemId, uint newDurability); event Used(address item, uint tokenId, address heroToken, uint heroTokenId); event Destroyed(address item, uint itemId); event FragilityReduced(address item, uint itemId, address consumedItem, uint consumedItemId, uint fragility); event ItemControllerHelper(address helper); event SetUnionConfig(uint configId, address[] items, uint[] count, address itemToMint); event RemoveUnionConfig(uint configId); event SetUnionKeyPass(address keyPassItem); event CombineItems(address msgSender, uint configId, address[] items, uint[][] itemIds, address mintedItem, uint mintedItemId); event RegisterSandboxItem(address hero, uint heroId, address item, uint itemId, uint tsMinting); event WithdrawItemsFromSandbox(address hero, uint heroId, address[] items, uint[] itemIds); event ItemReturnedToSandbox(address hero, uint heroId, address item, uint itemId); event RegisterSandboxUpgrade(address hero, uint heroId, uint tsUpgradng); event TransferItemToHeroFromSandbox(address hero, uint heroId, address item, uint itemId); event DestroyItemInSandbox(address item, uint itemId); event NewItemSentToSandbox(address item, uint itemId); event ExitFromDungeon(address hero, uint heroId); //endregion ------------------------ Item controller and helper contracts //region ------------------------ NFT and GameToken (only custom events, not ERC20/721 standards) event ChangePauseStatus(bool value); event MinterChanged(address value); event UniqueUriChanged(uint id, string uri); event BaseUriChanged(string uri); event HeroMinted(uint heroId); event HeroBurned(uint heroId); event HeroUriByStatusChanged(string uri, uint statusLvl); event ItemMinted(uint tokenId); event ItemBurned(uint tokenId); event UriByRarityChanged(string uri, uint rarity); event SponsoredHeroCreated(address msgSender, address heroAddress, uint heroId, string heroName); //endregion ------------------------ NFT and GameToken (only custom events, not ERC20/721 standards) //region ------------------------ User controller event SetUserName(address user, string name); event SetUserAvatar(address user, string avatar); event LootBoxOpened(address user, uint lootBoxKind, address[] itemTokens, uint[] itemTokenIds); event LootBoxConfigChanged(uint lootBoxKind, address[] mintItems, uint32[] mintItemsChances, uint maxDropItems); event SetFeeRenaming(uint feeRenaming); event ActivityCompleted(address user, bool daily, bool weekly); event RegisterPassedDungeon(address user, uint32 epochWeek, uint counterPassedDungeons); event RegisterPvp(address user, uint32 epochWeek, uint counterPvp); event FameHallHeroRegistered(address hero, uint heroId, address heroOwner, uint8 openedNgLevel); event SetMinHeroLevel(uint level); event SetGuildStakingAdapter(address adapter); event AddGamePoints(address user, uint finalBalanceGamePoints); /// @param paramId See IUserController.UserControllerParam event SetUserControllerParam(uint8 paramId, uint paramValue); event UseGamePointsToSkipStory(address user, uint16 storyId, uint priceInGamePoints, uint finalBalanceGamePoints); event SetStoryPassed(address user, uint16 storyId); //endregion ------------------------ User controller //region ------------------------ Guild event GuildCreated(address owner, uint guildId, string name, string urlLogo); event AddToGuild(uint guildId, address newUser); event ChangeGuildRights(uint guildId, address user, uint rights); event RemoveFromGuild(uint guildId, address user); event GuildDeleted(uint guildId); event GuildLevelUp(uint guildId, uint8 newLevel); event GuildRename(uint guildId, string newName); event GuildLogoChanged(uint guildId, string newLogoUrl); event GuildDescriptionChanged(uint guildId, string newDescription); event SetGuildRelation(uint guildId1, uint guildId2, bool peace); event TransferFromGuildBank(address user, address token, uint amount, address recipient); event TransferNftFromGuildBank(address user, address[] nfts, uint[] tokenIds, address recipient); event GuildBankDeployed(uint guildId, address guildBank); event TransferOwnership(address prevOwner, address newOwner); event SetToHelperRatio(uint guildId, uint8 value, address user); event TopUpGuildBank(address msgSender, uint guildId, address guildBank, uint amount); event GuildRequestRegistered(address msgSender, uint guildId, string userMessage, uint depositAmount); event GuildRequestStatusChanged(address msgSender, uint guildRequestId, uint8 newStatus, address user); event SetToHelperRatio(uint guildId, address msgSender, uint8 toHelperRatio); event SetGuildRequestDepositAmount(uint guildId, address msgSender, uint amount); event SetGuildBaseFee(uint fee); event SetPvpPointsCapacity(address msgSender, uint64 capacityPvpPoints, address[] users); event SetShelterController(address shelterController); event SetShelterAuction(address shelterAuction); event PayForBidFromGuildBank(uint guildId, uint amount, uint bid); //endregion ------------------------ Guild //region ------------------------ Guild shelter event RegisterShelter(uint sheleterId, uint price); event SetShelterItems( uint shelterId, address[] items, uint64[] pricesInPvpPoints, uint128[] pricesInGameTokens, uint16[] maxItemsPerDayThresholds ); event RemoveShelterItems(uint shelterId, address[] items); event BuyShelter(uint guidlId, uint shelterId); event LeaveShelter(uint guildId, uint shelterId); event NewShelterBid(uint shelterId, uint buyerGuildId, uint amount); event RevokeShelterBid(uint shelterId); event UseShelterBid(uint shelterId, uint sellerGuildId, uint buyerGuidId, uint amount); event PurchaseShelterItem(address msgSender, address item, uint numSoldItems, uint priceInPvpPoints, uint priceInGameToken); event ChangeShelterOwner(uint shelterId, uint fromGuildId, uint toGuildId); event RestInShelter(address msgSender, address heroToken, uint heroTokenId); //endregion ------------------------ Guild shelter //region ------------------------ Guild reinforcement event GuildHeroStaked(address heroToken, uint heroId, uint guildId); event GuildHeroWithdrawn(address heroToken, uint heroId, uint guildId); event GuildHeroAsked(address heroToken, uint heroId, uint guildId, address user); /// @param user Address can be 0 if heroId was already burnt at the moment of reinforcement releasing event GuildHeroReleased(address heroToken, uint heroId, uint guildId, address user); //endregion ------------------------ Guild reinforcement //region ------------------------ Pvp event AddBiomeRequest(address user, uint8 biome, uint guildId, uint32 week); event PvpHeroAdded(address user, uint guildId, address hero, uint heroId, uint week, uint8 biome); /// @param manuallyRemoved True - removed manually by the user, false - removed automatically after the fight event PvpHeroRemoved(address user, uint guildId, uint week, uint8 biome, address hero, uint heroId, bool manuallyRemoved); event PreparePvpFight(uint48 fightId, uint32 week, address hero, uint heroId, uint heroGuildId, address opponentHero, uint opponentHeroId, uint opponentGuildId); /// @notice heroId can be detected by {fightId} and {heroes} event PvpFightCompleted( IPvpController.PvpFightResults fightResult, uint48 fightId, address[2] heroes, uint64[2] guilds, bool[2] winners, uint[2] prizes, bool technicalDefeat ); event UpdatePvpEpoch(uint8 biome, uint32 week, uint guildBiomeOwnerId); event FirstPvpEpoch(uint8 biome, uint32 week); event BiomeTaxPaid(address msgSender, uint8 biome, uint guildId, uint amount, uint taxPercent, uint taxAmount); event BiomeTaxPaidNft(address msgSender, uint8 biome, uint guildId, address item, uint itemId, uint taxPercent); event AddPvpFightItems(uint48 fightId, address[] items, uint[] itemIds); //endregion ------------------------ Pvp //region ------------------------ Guild auction event AuctionPositionOpened(uint positionId, uint shelterId, uint sellerGuildId, address msgSender, uint minAuctionPrice); event AuctionPositionClosed(uint positionId, address msgSender); event AuctionBidOpened(uint bidId, uint positionId, uint amount, address msgSender); event ApplyAuctionBid(uint bidId, address msgSender); event AuctionSetFee(uint fee); //endregion ------------------------ Guild auction //region ------------------------ Guild bank event GuildBankTransfer(address token, address recipient, uint amount); event GuildBankTransferNft(address to, address nft, uint tokenId); event GuildBankTransferNftMulti(address to, address[] nfts, uint[] tokenIds); //endregion ------------------------ Guild bank //region ------------------------ Pawnshop event PawnShopRouterDeployed(address pawnShop, address gameToken, address routerOwner, address deployed); event PawnShopRouterTransfer(address token, uint amount, address receiver); event PawnShopRouterBulkSell(address[] nfts, uint[] nftIds, uint[] prices, address nftOwner, uint[] positionIds); event PawnShopRouterClosePositions(uint[] positionIds, address receiver); event PawnShopRouterBulkBuy(uint[] positionIds, address receiver); //endregion ------------------------ Pawnshop //region ------------------------ Airdrop Distributor event AirdropDistributorSetToken(address token); event AirdropDistributorAddTree(uint week, bytes32 merkleRoot_); event AirdropDistributorRemoveTree(uint week); event AirdropDistributorClaim(uint[] _weeks, uint[] amounts, address receiver); //endregion ------------------------ Airdrop Distributor //region ------------------------ GuildStakingManager event SetStakingToken(address token); event StakeTokens(address token, uint amount, uint guildId, uint total); //endregion ------------------------ GuildStakingManager }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IControllable { function VERSION() external pure returns (string memory); function revision() external view returns (uint); function previousImplementation() external view returns (address); function isController(address contract_) external view returns (bool); function isGovernance(address contract_) external view returns (bool); function created() external view returns (uint256); function createdBlock() external view returns (uint256); function controller() external view returns (address); function increaseRevision(address oldLogic) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IController { function governance() external view returns (address); function statController() external view returns (address); function storyController() external view returns (address); function gameObjectController() external view returns (address); function reinforcementController() external view returns (address); function oracle() external view returns (address); function treasury() external view returns (address); function itemController() external view returns (address); function heroController() external view returns (address); function dungeonFactory() external view returns (address); function gameToken() external view returns (address); function validTreasuryTokens(address token) external view returns (bool); function isDeployer(address adr) external view returns (bool); function onPause() external view returns (bool); function userController() external view returns (address); function guildController() external view returns (address); function pvpController() external view returns (address); function rewardsPool() external view returns (address); function itemBoxController() external view returns (address); function gameTokenPrice() external view returns (uint); function process(address token, uint amount, address from) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; interface IDungeonFactory { /// @custom:storage-location erc7201:dungeon.factory.main struct MainState { /// @dev biome => dungeonLaunchedId mapping(uint => EnumerableSet.UintSet) freeDungeons; /// @dev hero + heroId + biome (packMapObject) -> completed mapping(bytes32 => bool) bossCompleted; /// @dev hero + heroId + dungNum (packDungeonKey) -> completed mapping(bytes32 => bool) specificDungeonCompleted; /// @notice Max biome completed by the hero /// @dev hero + heroId (nftPacked) -> max biome completed mapping(bytes32 => uint8) maxBiomeCompleted; /// @notice which dungeon the hero is currently in /// @dev hero+id => current DungeonId mapping(bytes32 => uint64) heroCurrentDungeon; // --- /// @notice Specific dungeon for the given pair of hero level + hero class /// ALl specific dungeons are listed also in allSpecificDungeons /// @dev packUint8Array(specReqBiome, specReqHeroClass) => dungNum mapping(bytes32 => uint16) dungeonSpecific; /// @dev contains all specific dungNum for easy management EnumerableSet.UintSet allSpecificDungeons; /// @dev biome => dungNum mapping(uint8 => EnumerableSet.UintSet) dungeonsLogicByBiome; // --- /// @dev max available biome. auto-increment with new dung deploy uint8 maxBiome; /// @notice Address of treasure token => min hero level required /// @dev manual threshold for treasury mapping(address => uint) minLevelForTreasury; /// @notice Contains arrays for SKILL_1, SKILL_2, SKILL_3 with 0 or 1 /// i.e. [0, 1, 0] means that durability of SKILL_2 should be reduced /// @dev hero + heroId => uint8[] array where idx = slotNum mapping(bytes32 => bytes32) skillSlotsForDurabilityReduction; /// @notice Counter of dungeons, it's incremented on launch of a new dungeon uint64 dungeonCounter; /// @dev dungNum = init attributes mapping(uint16 => DungeonAttributes) dungeonAttributes; /// @dev dungeonId => status mapping(uint64 => DungeonStatus) dungeonStatuses; /// @notice NG_LEVEL of the hero that has created the given dungeon mapping(uint64 dungeonId => uint ngLevel) dungeonNgLevel; } struct ObjectGenerateInfo { /// @notice List of chamber types for each unique object /// @dev uint8 types, packed using PackingLib.packUint8Array bytes32[] objTypesByStages; /// @notice List of chances for each chamber type /// @dev uint64 chances uint32[][] objChancesByStages; } struct DungeonGenerateInfo { /// @notice List of chamber types for each unique object uint8[][] objTypesByStages; /// @notice List of chances for each chamber type uint32[][] objChancesByStages; uint32[] uniqObjects; uint8 minLevel; uint8 maxLevel; bytes32[] requiredCustomDataIndex; uint64[] requiredCustomDataMinValue; uint64[] requiredCustomDataMaxValue; bool[] requiredCustomDataIsHero; } /// @notice Attributes of the given dungeon logic struct DungeonAttributes { /// @notice Total number of stages that should be passed to complete the dungeon uint8 stages; uint8 biome; /// @notice Default list of objects that should be passed in the dungeon uint32[] uniqObjects; /// @dev min+max (packUint8Array) bytes32 minMaxLevel; bytes32[] requiredCustomDataIndex; /// @notice Packed DungeonGenerateInfo.requiredCustomData: MinValue, MaxValue, IsHero /// @dev min+max+isHero(packStoryCustomDataRequirements) bytes32[] requiredCustomDataValue; ObjectGenerateInfo info; } /// @notice Current status of the given dungeon struct DungeonStatus { uint64 dungeonId; /// @notice Dungeon logic id uint16 dungNum; /// @notice True if the dungeon is completed by the hero bool isCompleted; /// @notice Hero in the dungeon or 0 address heroToken; uint heroTokenId; /// @notice Current object that should be passed by the hero. 0 - new object is not opened uint32 currentObject; /// @notice Current stage in the dungeon that should be passed by the hero. uint8 currentStage; EnumerableMap.AddressToUintMap treasuryTokens; /// @notice All items that were minted on result of made actions bytes32[] treasuryItems; /// @notice Total number of stages that should be passed to complete the dungeon /// This value can be bigger than length of uniqObjects uint8 stages; /// @notice List of objects to be passed in the stage. The list can be dynamically changed during passing the stages uint32[] uniqObjects; } //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// function launchForNewHero(address heroToken, uint heroTokenId, address owner) external returns (uint64 dungeonId); function maxBiomeCompleted(address heroToken, uint heroTokenId) external view returns (uint8); function currentDungeon(address heroToken, uint heroTokenId) external view returns (uint64); function skillSlotsForDurabilityReduction(address heroToken, uint heroTokenId) external view returns (uint8[] memory result); function setBossCompleted(uint32 objectId, address heroToken, uint heroTokenId, uint8 heroBiome) external; /// @notice Hero exists current dungeon forcibly same as when dying but without loosing life chance function exitForcibly(address heroToken, uint heroTokenId, address msgSender) external; function maxAvailableBiome() external view returns (uint8); function reborn(address hero, uint heroId) external; }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @dev Interface of the ERC165 standard, as defined in the * https://eips.ethereum.org/EIPS/eip-165[EIP]. * * Implementers can declare support of contract interfaces, which can then be * queried by others ({ERC165Checker}). * * For an implementation, see {ERC165}. */ interface IERC165 { /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] * to learn more about how these ids are created. * * This function call must use less than 30 000 gas. */ function supportsInterface(bytes4 interfaceId) external view returns (bool); }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @dev Interface of the ERC20 standard as defined in the EIP. */ interface IERC20 { /** * @dev Returns the amount of tokens in existence. */ function totalSupply() external view returns (uint256); /** * @dev Returns the amount of tokens owned by `account`. */ function balanceOf(address account) external view returns (uint256); /** * @dev Moves `amount` tokens from the caller's account to `recipient`. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transfer(address recipient, uint256 amount) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */ function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. * * Returns a boolean value indicating whether the operation succeeded. * * IMPORTANT: Beware that changing an allowance with this method brings the risk * that someone may use both the old and the new allowance by unfortunate * transaction ordering. One possible solution to mitigate this race * condition is to first reduce the spender's allowance to 0 and set the * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * * Emits an {Approval} event. */ function approve(address spender, uint256 amount) external returns (bool); /** * @dev Moves `amount` tokens from `sender` to `recipient` using the * allowance mechanism. `amount` is then deducted from the caller's * allowance. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transferFrom( address sender, address recipient, uint256 amount ) external returns (bool); /** * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ event Approval(address indexed owner, address indexed spender, uint256 value); }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./IERC165.sol"; /** * @dev Required interface of an ERC721 compliant contract. */ interface IERC721 is IERC165 { /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. */ event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. */ event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /** * @dev Returns the number of tokens in ``owner``'s account. */ function balanceOf(address owner) external view returns (uint256 balance); /** * @dev Returns the owner of the `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function ownerOf(uint256 tokenId) external view returns (address owner); /** * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients * are aware of the ERC721 protocol to prevent tokens from being forever locked. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId ) external; /** * @dev Transfers `tokenId` token from `from` to `to`. * * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. * * Emits a {Transfer} event. */ function transferFrom( address from, address to, uint256 tokenId ) external; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. * The approval is cleared when the token is transferred. * * Only a single account can be approved at a time, so approving the zero address clears previous approvals. * * Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. * * Emits an {Approval} event. */ function approve(address to, uint256 tokenId) external; /** * @dev Returns the account approved for `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function getApproved(uint256 tokenId) external view returns (address operator); /** * @dev Approve or remove `operator` as an operator for the caller. * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. * * Requirements: * * - The `operator` cannot be the caller. * * Emits an {ApprovalForAll} event. */ function setApprovalForAll(address operator, bool _approved) external; /** * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. * * See {setApprovalForAll} */ function isApprovedForAll(address owner, address operator) external view returns (bool); /** * @dev Safely transfers `tokenId` token from `from` to `to`. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data ) external; }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title ERC721 token receiver interface * @dev Interface for any contract that wants to support safeTransfers * from ERC721 asset contracts. */ interface IERC721Receiver { /** * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} * by `operator` from `from`, this function is called. * * It must return its Solidity selector to confirm the token transfer. * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. * * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. */ function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IStatController.sol"; import "./IItemController.sol"; interface IFightCalculator { enum AttackType { UNKNOWN, // 0 MELEE, // 1 MAGIC, // 2 SLOT_3, SLOT_4, SLOT_5, SLOT_6, SLOT_7, SLOT_8, SLOT_9, SLOT_10 } /// @notice Attacker info: suitable both for hero and monsters struct AttackInfo { /// @notice Type of the attack /// by default, if attack token presents, it's magic attack and not-magic otherwise /// but this logic can become more complicated after introducing new attack types AttackType attackType; /// @notice NFT selected by hero for attack, it should be equip on. /// If attacker is a monster, this is a special case (stub NFT with zero ID is used) address attackToken; uint attackTokenId; address[] skillTokens; uint[] skillTokenIds; } struct FighterInfo { int32[] fighterAttributes; IStatController.ChangeableStats fighterStats; AttackType attackType; address attackToken; uint attackTokenId; uint race; } struct Statuses { bool stun; bool burn; bool freeze; bool confuse; bool curse; bool poison; bool gotCriticalHit; bool missed; bool hitBlocked; } struct FightResult { int32 healthA; int32 healthB; int32 manaConsumedA; int32 manaConsumedB; } struct FightCall { FighterInfo fighterA; FighterInfo fighterB; uint64 dungeonId; uint32 objectId; address heroAdr; uint heroId; uint8 stageId; uint iteration; uint8 turn; } /// @notice Additional info passed to fight struct FightCallAdd { address msgSender; /// @notice Unique ID of the pvp-fight, 0 for not pvp fights uint48 fightId; } struct SkillSlots { bool slot1; bool slot2; bool slot3; } //region ------------------------ FightLib-internal (FightInfoInternal is required by IApplicationEvents..) struct FightInfoInternal { Fighter fighterA; Fighter fighterB; } struct Fighter { IFightCalculator.FighterInfo info; IItemController.AttackInfo magicAttack; int32 health; int32 manaConsumed; int32 damage; int32 damagePoison; int32 damageReflect; IFightCalculator.Statuses statuses; } //endregion ------------------------ FightLib-internal function fight(FightCall memory callData) external returns (FightResult memory); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IERC20.sol"; interface IGameToken is IERC20 { function minter() external view returns (address); function mint(address account, uint amount) external returns (bool); function burn(uint amount) external returns (bool); function setMinter(address minter_) external; function pause(bool value) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "./IController.sol"; interface IGOC { enum ObjectType { UNKNOWN, // 0 EVENT, // 1 MONSTER, // 2 STORY, // 3 END_SLOT } enum ObjectSubType { UNKNOWN_0, // 0 ENEMY_NPC_1, // 1 ENEMY_NPC_SUPER_RARE_2, // 2 BOSS_3, // 3 SHRINE_4, // 4 CHEST_5, // 5 STORY_6, // 6 STORY_UNIQUE_7, // 7 SHRINE_UNIQUE_8, // 8 CHEST_UNIQUE_9, // 9 ENEMY_NPC_UNIQUE_10, // 10 STORY_ON_ROAD_11, // 11 STORY_UNDERGROUND_12, // 12 STORY_NIGHT_CAMP_13, // 13 STORY_MOUNTAIN_14, // 14 STORY_WATER_15, // 15 STORY_CASTLE_16, // 16 STORY_HELL_17, // 17 STORY_SPACE_18, // 18 STORY_WOOD_19, // 19 STORY_CATACOMBS_20, // 20 STORY_BAD_HOUSE_21, // 21 STORY_GOOD_TOWN_22, // 22 STORY_BAD_TOWN_23, // 23 STORY_BANDIT_CAMP_24, // 24 STORY_BEAST_LAIR_25, // 25 STORY_PRISON_26, // 26 STORY_SWAMP_27, // 27 STORY_INSIDE_28, // 28 STORY_OUTSIDE_29, // 29 STORY_INSIDE_RARE_30, STORY_OUTSIDE_RARE_31, ENEMY_NPC_INSIDE_32, ENEMY_NPC_INSIDE_RARE_33, ENEMY_NPC_OUTSIDE_34, ENEMY_NPC_OUTSIDE_RARE_35, END_SLOT } /// @custom:storage-location erc7201:game.object.controller.main struct MainState { /// @dev objId = biome(00) type(00) id(0000) => biome(uint8) + objType(uint8) /// Id is id of the event, story or monster. mapping(uint32 => bytes32) objectMeta; /// @dev biome(uint8) + objType(uint8) => set of object id mapping(bytes32 => EnumerableSet.UintSet) objectIds; /// @dev heroAdr180 + heroId64 + cType8 + biome8 => set of already played objects. Should be cleared periodically mapping(bytes32 => EnumerableSet.UintSet) playedObjects; /// @dev HeroAdr(160) + heroId(uint64) + objId(uint32) => iteration count. It needs for properly emit events for every new entrance. mapping(bytes32 => uint) iterations; /// @dev objId(uint32) => EventInfo mapping(uint32 => EventInfo) eventInfos; /// @dev objId(uint32) => storyId mapping(uint32 => uint16) storyIds; /// @dev objId(uint32) => MonsterInfo mapping(uint32 => MonsterInfo) monsterInfos; /// @dev hero+id => last fight action timestamp mapping(bytes32 => uint) lastHeroFightTs; /// @dev delay for user actions in fight (suppose to prevent bot actions) uint fightDelay; } struct ActionResult { bool kill; bool completed; address heroToken; address[] mintItems; int32 heal; int32 manaRegen; int32 lifeChancesRecovered; int32 damage; int32 manaConsumed; uint32 objectId; uint32 experience; uint heroTokenId; uint iteration; uint32[] rewriteNextObject; } struct EventInfo { /// @dev chance to use good or bad attributes/stats uint32 goodChance; /// @dev toBytes32ArrayWithIds bytes32[] goodAttributes; bytes32[] badAttributes; /// @dev experience(uint32) + heal(int32) + manaRegen(int32) + lifeChancesRecovered(int32) + damage(int32) + manaConsume(int32) packStatsChange bytes32 statsChange; /// @dev item+chance packItemMintInfo bytes32[] mintItems; } struct MonsterInfo { /// @dev toBytes32ArrayWithIds bytes32[] attributes; /// @dev level(uint8) + race(uint8) + experience(uint32) + maxDropItems(uint8) packMonsterStats bytes32 stats; /// @dev attackToken(160) + attackTokenId(uint64) + attackType(uint8) packAttackInfo bytes32 attackInfo; /// @dev item+chance packItemMintInfo bytes32[] mintItems; /// @dev heroAdr(160) + heroId(uint64) => iteration => GeneratedMonster packed mapping(bytes32 => mapping(uint => bytes32)) _generatedMonsters; } struct MultiplierInfo { uint8 biome; /// @notice NG_LEVEL of the hero who is going to fight with the given monster /// Use type(uint8).max for !NG+ uint8 heroNgLevel; } struct GeneratedMonster { bool generated; uint8 turnCounter; int32 hp; uint32 amplifier; } struct MonsterGenInfo { uint16 monsterId; uint8 biome; ObjectSubType subType; uint8[] attributeIds; int32[] attributeValues; uint8 level; uint8 race; uint32 experience; uint8 maxDropItems; address attackToken; uint64 attackTokenId; uint8 attackType; address[] mintItems; uint32[] mintItemsChances; } struct ActionContext { address sender; address heroToken; IController controller; uint8 biome; uint8 objectSubType; uint8 stageId; uint8 heroNgLevel; uint32 objectId; uint64 dungeonId; uint heroTokenId; uint salt; uint iteration; bytes data; } struct EventRegInfo { uint8 biome; uint16 eventId; ObjectSubType subType; uint32 goodChance; AttributeGenerateInfo goodAttributes; AttributeGenerateInfo badAttributes; uint32 experience; int32 heal; int32 manaRegen; int32 lifeChancesRecovered; int32 damage; int32 manaConsumed; address[] mintItems; uint32[] mintItemsChances; } struct AttributeGenerateInfo { uint8[] ids; int32[] values; } ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// /// @dev represent object registration if non zero values function getObjectMeta(uint32 objectId) external view returns (uint8 biome, uint8 objectSubType); function isBattleObject(uint32 objectId) external view returns (bool); function getRandomObject( uint8[] memory cTypes, uint32[] memory chances, uint8 biomeLevel, address heroToken, uint heroTokenId ) external returns (uint32 objectId); function open(address heroToken, uint heroTokenId, uint32 objectId) external returns (uint iteration); function action( address sender, uint64 dungeonId, uint32 objectId, address heroToken, uint heroTokenId, uint8 stageId, bytes memory data ) external returns (ActionResult memory); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IERC20.sol"; import "../interfaces/IERC721.sol"; interface IGuildBank { function transfer(address token, address recipient, uint amount) external; function approve(address token, address spender, uint256 amount) external returns (bool); function transferNft(address to, address nft, uint256 tokenId) external; function transferNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external; function approveNft(address to, address nft, uint256 tokenId) external; function approveNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; interface IGuildController { enum GuildRightBits { ADMIN_0, RENAME_1, CHANGE_LOGO_2, CHANGE_SHELTER_3, ADD_MEMBER_4, REMOVE_MEMBER_5, BANK_TOKENS_OPERATION_6, CHANGE_ROLES_7, LEVEL_UP_8, SET_RELATION_KIND_9, BANK_ITEMS_OPERATION_10, SET_GUILD_PARAMS_11, CHANGE_PURCHASING_SHELTER_ITEMS_CAPACITY_12, DOMINATION_REQUEST_13 } enum GuildsParams { NONE_0, COUNTER_GUILD_IDS_1, BASE_FEE_2, COUNTER_GUILD_REQUESTS_3, REENTRANT_STATUS_4, SHELTER_CONTROLLER_5, SHELTER_AUCTION_6 // max 255 params because enum is uint8 by default } enum GuildRequestStatus { NONE_0, ACCEPTED_1, REJECTED_2, CANCELED_3 } /// @custom:storage-location erc7201:guild.controller.main struct MainState { /// @notice Mapping to store various guilds params (with global values for all guilds) mapping(GuildsParams param => uint value) guildsParam; /// @notice guildId => address of instance of GuildBank contract mapping(uint guildId => address) guildBanks; /// @notice guild id => guild data (owner, name, logo, etc) mapping(uint guildId => GuildData) guildData; /// @notice name => guild id mapping(string guildName => uint guildId) nameToGuild; /// @notice EOA => guild id, EOA can be a member of a single guild only mapping(address member => uint guildId) memberToGuild; /// @notice List of participants of guilds /// @dev Allowed number of members is 20 + 5 * guildLevel mapping(uint guildId => EnumerableSet.AddressSet listEoa) members; /// @notice Rights of the member in the guild, mask of GuildRightBits mapping(address member => uint maskRights) rights; /// @notice _getGuildsPairKey(guild1, guild2) => status (false - war, true - peace) mapping(bytes32 guildsPairKey => bool) relationsPeaceful; // ---------------------------- Request to join to the guild /// @notice Full list of requests registered for the guild mapping(uint guildId => mapping(GuildRequestStatus status => EnumerableSet.UintSet guildRequestIds)) guildRequests; /// @notice List of active requests created by the given user. /// "Active" => deposit should be returned to the user. /// All not-active requests are removed from here automatically. mapping(address user => EnumerableSet.UintSet guildRequestIds) userActiveGuildRequests; /// @notice Data of all guild requests ever created mapping(uint guildRequestId => GuildRequestData) guildRequestData; /// @notice Deposit amount required to create a guild request mapping(uint guildId => GuildRequestDeposit) guildRequestDepositAmounts; /// @notice Counter of spent pvp points + number of guild pvp-points allowed to be used by the guild member mapping(uint guildId => mapping(address member => UserPvpPoints)) userPvpPoints; /// @notice guild id => guildDescription mapping(uint guildId => string) guildDescription; } struct GuildData { /// @notice Not empty unique guild name string guildName; /// @notice URL of guild logo (empty is allowed) string urlLogo; /// @notice Creator (owner) of the guild address owner; /// @notice Guild level [1...10] uint8 guildLevel; /// @notice Percent of guild reinforcement fee Value in range [_FEE_MIN ... _TO_HELPER_RATIO_MAX], i.e. [10..50] uint8 toHelperRatio; /// @notice Global guild points counter, it's incremented on each victory in php-fight. /// @dev Assume here, that uint64 is enough to store any sums of scores uint64 pvpCounter; } struct GuildRequestData { GuildRequestStatus status; /// @notice Creator of the guild request that asks to include him to the guild address user; /// @notice Message to the guild owner from the user string userMessage; uint guildId; } struct GuildRequestDeposit { bool initialized; uint192 amount; } struct UserPvpPoints { /// @notice How many guild pvp-points the user is allowed to use uint64 capacityPvpPoints; /// @notice How many guild pvp-points the user has used uint64 spentPvpPoints; } /// ---------------------------------------------------------------------------------------------- function memberOf(address user) external view returns (uint guildId); function guildToShelter(uint guildId) external view returns (uint shelterId); function getGuildData(uint guildId) external view returns ( string memory guildName, string memory urlLogo, address owner, uint8 guildLevel, uint64 pvpCounter, uint toHelperRatio ); function getRights(address user) external view returns (uint); function getGuildBank(uint guildId) external view returns (address); function shelterController() external view returns (address); function isPeacefulRelation(uint guildId, uint guildId2) external view returns (bool); function incPvpCounter(uint guildId, uint64 value) external; function usePvpPoints(uint guildId, address user, uint64 priceInPvpPoints) external; function payFromGuildBank(uint guildId, uint shelterPrice) external; function payFromBalance(uint amount, address user) external; /// @notice Ensure that the {user} has given {right}, revert otherwise function checkPermissions(address user, uint right) external view returns (uint guildId, uint rights); function shelterAuctionController() external view returns (address); function payForAuctionBid(uint guildId, uint amount, uint bid) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IGuildStakingAdapter { /// @notice Calculate relative increment of the biome tax for the given guild owner, [0..1e18] /// 0 - no increment (default 1% is used), 1 - max possible increment (i.e. 5%) function getExtraFeeRatio(uint guildId) external view returns (uint); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; interface IHeroController { /// @custom:storage-location erc7201:hero.controller.main struct MainState { /// @dev A central place for all hero tokens /// @dev Deprecated. Controller is used instead. address heroTokensVault; /// @dev heroAdr => packed tokenAdr160+ amount96 mapping(address => bytes32) payToken; /// @dev heroAdr => heroCls8 mapping(address => uint8) heroClass; // --- /// @dev hero+id => individual hero name mapping(bytes32 => string) heroName; /// @dev name => hero+id, needs for checking uniq names mapping(string => bytes32) nameToHero; // --- /// @dev hero+id => biome mapping(bytes32 => uint8) heroBiome; /// @notice Exist reinforcement of any kind for the given hero /// @dev hero+id => packed reinforcement helper+id mapping(bytes32 => bytes32) reinforcementHero; /// @dev hero+id => reinforcement packed attributes mapping(bytes32 => bytes32[]) reinforcementHeroAttributes; /// @notice packedHero (hero + id) => count of calls of beforeTokenTransfer mapping(bytes32 => uint) countHeroTransfers; // ------------------------------------ NG plus /// @notice (tier, hero address) => TierInfo, where tier = [2, 3] /// @dev For tier=1 no data is required. Amount for tier 1 is stored in {payToken}, no items are minted /// Token from {payToken} is equal for all tiers mapping(bytes32 packedTierHero => TierInfo) tiers; mapping(bytes32 packedHero => HeroInfo) heroInfo; /// @notice Max NG_LVL reached by the heroes of a given account mapping(address user => uint8 maxNgLevel) maxUserNgLevel; /// @notice When the hero has killed boss on the given biome first time /// packedBiomeNgLevel = packed (biome, NG_LEVEL) mapping(bytes32 packedHero => mapping (bytes32 packedBiomeNgLevel => uint timestamp)) killedBosses; /// @notice Max NG_LEVEL reached by any user uint maxOpenedNgLevel; /// @notice Sandbox mode for heroes, see SCR-1153 mapping(bytes32 packedHero => SandboxMode sandboxMode) sandbox; } /// @notice Tier = hero creation cost option /// There are 3 tiers: /// 1: most chip option, just pay fixed amount {payTokens} - new hero is created /// 2: pay bigger amount - random skill is equipped on the newly created hero /// 3: pay even more amount - random sill + some random items are equipped on the newly created hero struct TierInfo { /// @notice Cost of the hero creation using the given tier in terms of the token stored in {payToken} /// This amount is used for tiers 2, 3. For tier 1 the amount is taken from {payToken} uint amount; /// @notice All slots for which items-to-mint are registered in {itemsToMint} EnumerableSet.UintSet slots; /// @notice slot => items that can be minted and equipped on the hero to the given {slot} after hero creation mapping(uint8 slot => address[] items) itemsToMint; } /// @notice Current NG+-related values struct HeroInfo { /// @notice Hero tier = [0..3]. 0 - the hero is post-paid, it can be changed by upgrading the hero to pre-paid uint8 tier; /// @notice NG_LVL of the hero uint8 ngLevel; /// @notice True if hero has passed last biome on current NG+ and so NG_LEVEL can be incremented (reborn is allowed) bool rebornAllowed; /// @notice Amount paid for the hero on creation OR on upgrade to NG+ /// Amount paid for creation of the hero in terms of game token (!NG+) is NOT stored here. /// @dev uint72 is used here to pack the whole struct to single slot /// Zero for sandbox-heroes uint72 paidAmount; /// @notice Pay token used to pay {paidAmount} /// Zero for sandbox-heroes address paidToken; } /// @notice Input data to create new hero struct HeroCreationData { /// @notice Desired NG_LVL of the hero uint8 ngLevel; /// @notice Desired tire of the newly created hero. Allowed values: [1..3] uint8 tier; /// @notice Enter to the dungeon after creation bool enter; /// @notice Desired hero name string heroName; /// @notice Optional: user account for which the hero is created address targetUserAccount; /// @notice Optional: ref-code to be passed to the hero-creation-related event string refCode; /// @notice SCR-1153: create not-paid hero with limited rights bool sandboxMode; } enum SandboxMode { /// @notice The hero is created in normal (not sandbox) mode NORMAL_MODE_0, /// @notice The hero was created in sandbox mode and wasn't upgraded. SANDBOX_MODE_1, /// @notice The hero was created in sandbox mode and was upgraded to the normal mode UPGRADED_TO_NORMAL_2 } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function heroClass(address hero) external view returns (uint8); function heroBiome(address hero, uint heroId) external view returns (uint8); function payTokenInfo(address hero) external view returns (address token, uint amount); function heroReinforcementHelp(address hero, uint heroId) external view returns (address helperHeroToken, uint helperHeroId); function score(address hero, uint heroId) external view returns (uint); function isAllowedToTransfer(address hero, uint heroId) external view returns (bool); function beforeTokenTransfer(address hero, uint heroId) external returns (bool); // --- function create(address hero, string memory heroName_, bool enter) external returns (uint); function kill(address hero, uint heroId) external returns (bytes32[] memory dropItems); function releaseReinforcement(address hero, uint heroId) external returns (address helperToken, uint helperId); function resetLifeAndMana(address hero, uint heroId) external; function countHeroTransfers(address hero, uint heroId) external view returns (uint); function askGuildReinforcement(address hero, uint heroId, address helper, uint helperId) external; function getHeroInfo(address hero, uint heroId) external view returns (IHeroController.HeroInfo memory data); function registerKilledBoss(address hero, uint heroId, uint32 objectId) external; function maxOpenedNgLevel() external view returns (uint); function sandboxMode(address hero, uint heroId) external view returns (uint8); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IItem { function isItem() external pure returns (bool); function mintFor(address recipient) external returns (uint tokenId); function burn(uint tokenId) external; function controlledTransfer(address from, address to, uint tokenId) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; interface IItemBoxController { /// @custom:storage-location erc7201:ItemBox.controller.main struct MainState { mapping(bytes32 packedHero => HeroData) heroData; /// @notice Owners of all items minted in sandbox mode mapping(bytes32 packedItem => bytes32 packedHero) heroes; } struct HeroData { /// @notice Moment of upgrading sandbox-hero to normal-hero uint tsUpgraded; /// @notice List of all items registered for the hero EnumerableSet.AddressSet items; /// @notice item => (itemId => packedItemBoxItemInfo) /// @dev Ids are never deleted from the map, so the order of ids is never changed mapping(address item => EnumerableMap.UintToUintMap) states; } struct ItemBoxItemInfo { /// @notice True if the item was withdrawn from balance /// It can happens in follow cases: /// 1) the hero was upgraded and the item was withdrawn on hero owner balance /// 2) the item is used by ItemController: /// 2.1) the item is equipped on the hero and so it's transferred to the hero balance /// 2.2) the consumable item is used /// 3) the item is burnt /// @dev Status is required to avoid deletion (and so changing order) of the {items} bool withdrawn; /// @notice The moment of the initial item minting uint64 timestamp; } enum ItemState { /// @notice The item was never registered in the sandbox NOT_REGISTERED_0, /// @notice The item is not active (outdated) and cannot be used anymore NOT_AVAILABLE_1, /// @notice The item is active and located inside the sandbox INSIDE_2, /// @notice The item is either withdrawn or equipped OUTSIDE_3 } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function firstActiveItemOfHeroByIndex(address hero, uint heroId, address item) external view returns (uint itemId); function registerItems(address hero, uint heroId, address[] memory items, uint[] memory itemIds, uint countValidItems) external; function itemState(address hero, uint heroId, address item, uint itemId) external view returns (IItemBoxController.ItemState); function itemHero(address item, uint itemId) external view returns (address hero, uint heroId); function registerSandboxUpgrade(bytes32 packedHero) external; function transferToHero(address hero, uint heroId, address item, uint itemId) external; function destroyItem(address item, uint itemId) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IStatController.sol"; import "./IGOC.sol"; import "../openzeppelin/EnumerableSet.sol"; interface IItemController { enum GlobalParam { UNKNOWN_0, /// @notice Address of ItemControllerHelper ITEM_CONTROLLER_HELPER_ADDRESS_1 } /// @custom:storage-location erc7201:item.controller.main struct MainState { ////////////////// GENERATE ////////////////// EnumerableSet.AddressSet items; /// @dev itemAdr => itemMetaType8 + itemLvl8 + itemType8 + baseDurability16 + defaultRarity8 + minAttr8 + maxAttr8 + manaCost32 + req(packed core 128) mapping(address => bytes32) itemMeta; /// @dev itemAdr => packed tokenAdr160+ amount96 mapping(address => bytes32) augmentInfo; // --- common attr --- /// @dev itemAdr => id8 + min(int32) + max(int32) + chance32 mapping(address => bytes32[]) generateInfoAttributes; // --- consumable --- /// @dev itemAdr => ids+values (toBytes32ArrayWithIds) mapping(address => bytes32[]) _itemConsumableAttributes; /// @dev itemAdr => IStatController.ChangeableStats packed int32[] mapping(address => bytes32) itemConsumableStats; // --- buff --- /// @dev itemAdr => id8 + min(int32) + max(int32) + chance32 mapping(address => bytes32[]) generateInfoCasterAttributes; /// @dev itemAdr => id8 + minDmg(int32) + maxDmg(int32) + chance32 mapping(address => bytes32[]) generateInfoTargetAttributes; // --- attack --- /// @dev itemAdr => packed AttackInfo: attackType8 + min32 + max32 + factors(packed core 128) mapping(address => bytes32) generateInfoAttack; ////////////////// ITEMS INFO ////////////////// /// @dev itemAdr+id => itemRarity8 + augmentationLevel8 + itemDurability16 mapping(bytes32 => bytes32) itemInfo; /// @dev itemAdr+id => heroAdr+id mapping(bytes32 => bytes32) equippedOn; // --- common attr --- /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds) mapping(bytes32 => bytes32[]) _itemAttributes; // --- consumable --- // consumable stats unchangeable, get them by address // --- buff --- /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds) mapping(bytes32 => bytes32[]) _itemCasterAttributes; /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds) mapping(bytes32 => bytes32[]) _itemTargetAttributes; // --- attack --- /// @dev itemAdr+Id => packed AttackInfo: attackType8 + min32 + max32 + factors(packed core 128) mapping(bytes32 => bytes32) _itemAttackInfo; ////////////////// Additional generate info ////////////////// /// @notice (itemAdr) => Bitmask of ConsumableActionBits mapping(address => uint) _consumableActionMask; /// --------------------------------- SIP-003: Item fragility /// @notice itemAdr + id => item fragility counter that displays the chance of an unsuccessful repair /// @dev [0...100_000], decimals 3 mapping(bytes32 packedItem => uint fragility) itemFragility; /// @notice Universal mapping to store various addresses and numbers (params of the contract) mapping (GlobalParam param => uint value) globalParam; /// @notice Item address => packedMetadata /// {packedMetaData} is encoded using abi.encode/abi.decode /// Read first byte, detect meta data type by the byte value, apply proper decoder from PackingLib mapping(address item => bytes packedMetaData) packedItemMetaData; } struct RegisterItemParams { ItemMeta itemMeta; address augmentToken; uint augmentAmount; ItemGenerateInfo commonAttributes; IGOC.AttributeGenerateInfo consumableAttributes; IStatController.ChangeableStats consumableStats; ItemGenerateInfo casterAttributes; ItemGenerateInfo targetAttributes; AttackInfo genAttackInfo; /// @notice Bit mask of ConsumableActionBits uint consumableActionMask; } /// @notice Possible actions that can be triggered by using the consumable item enum ConsumableActionBits { CLEAR_TEMPORARY_ATTRIBUTES_0 // other items are used instead this mask } struct ItemGenerateInfo { /// @notice Attribute ids uint8[] ids; /// @notice Min value of the attribute, != 0 int32[] mins; /// @notice Max value of the attribute, != 0 int32[] maxs; /// @notice Chance of the selection [0..MAX_CHANCES] uint32[] chances; } struct ItemMeta { uint8 itemMetaType; // Level in range 1-99. Reducing durability in low level dungeons. lvl/5+1 = biome uint8 itemLevel; IItemController.ItemType itemType; uint16 baseDurability; uint8 defaultRarity; uint32 manaCost; // it doesn't include positions with 100% chance uint8 minRandomAttributes; uint8 maxRandomAttributes; IStatController.CoreAttributes requirements; } // Deprecated. Todo - remove enum FeeType { UNKNOWN, REPAIR, AUGMENT, STORY, END_SLOT } enum ItemRarity { UNKNOWN, // 0 NORMAL, // 1 MAGIC, // 2 RARE, // 3 SET, // 4 UNIQUE, // 5 END_SLOT } enum ItemType { NO_SLOT, // 0 HEAD, // 1 BODY, // 2 GLOVES, // 3 BELT, // 4 AMULET, // 5 RING, // 6 OFF_HAND, // 7 BOOTS, // 8 ONE_HAND, // 9 TWO_HAND, // 10 SKILL, // 11 OTHER, // 12 END_SLOT } enum ItemMetaType { UNKNOWN, // 0 COMMON, // 1 ATTACK, // 2 BUFF, // 3 CONSUMABLE, // 4 END_SLOT } enum AttackType { UNKNOWN, // 0 FIRE, // 1 COLD, // 2 LIGHTNING, // 3 CHAOS, // 4 END_SLOT } struct AttackInfo { AttackType aType; int32 min; int32 max; // if not zero - activate attribute factor for the attribute IStatController.CoreAttributes attributeFactors; } struct ItemInfo { ItemRarity rarity; uint8 augmentationLevel; uint16 durability; } /// @dev The struct is used in events, so it's moved here from the lib struct MintInfo { IItemController.ItemMeta meta; uint8[] attributesIds; int32[] attributesValues; IItemController.ItemRarity itemRarity; IItemController.AttackInfo attackInfo; uint8[] casterIds; int32[] casterValues; uint8[] targetIds; int32[] targetValues; } /// @dev The struct is used in events, so it's moved here from the lib struct AugmentInfo { uint8[] attributesIds; int32[] attributesValues; IItemController.AttackInfo attackInfo; uint8[] casterIds; int32[] casterValues; uint8[] targetIds; int32[] targetValues; } ///region ------------------------ Item type "Other" /// @notice Possible kinds of "Other" items /// Each "Other" item has each own structure for metadata, see OtherItemXXX enum OtherSubtypeKind { UNKNOWN_0, /// @notice Item to reduce fragility, see SCB-1014. Metadata is {OtherItemReduceFragility} REDUCE_FRAGILITY_1, /// @notice This item allows asking guild reinforcement to the guild member USE_GUILD_REINFORCEMENT_2, /// @notice Exit from dungeon (shelter of level 3 is required) EXIT_FROM_DUNGEON_3, /// @notice OTHER_5 Rest in the shelter: restore of hp & mp, clear temporally attributes, clear used consumables (shelter of level 3 is required) /// @dev It's OTHER_5 in deploy script, but internally it has subtype 4, see gen_others.ts REST_IN_SHELTER_4, /// @notice OTHER_4 Stub item that has no logic in contracts, but it has correct (not empty) packedMetaData /// @dev It's OTHER_4 in deploy script, but internally it has subtype 5, see gen_others.ts EMPTY_NO_LOGIC_5, END_SLOT } struct OtherItemReduceFragility { /// @notice "Other" item kind. It MUST BE first field in the struct. uint8 kind; /// @notice Value on which the fragility will be reduced. /// @dev [0...100%], decimals 3, so the value is in the range [0...10_000] uint248 value; } ///endregion ------------------------ Item type "Other" //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// function itemMeta(address item) external view returns (ItemMeta memory meta); function augmentInfo(address item) external view returns (address token, uint amount); function genAttributeInfo(address item) external view returns (ItemGenerateInfo memory info); function genCasterAttributeInfo(address item) external view returns (ItemGenerateInfo memory info); function genTargetAttributeInfo(address item) external view returns (ItemGenerateInfo memory info); function genAttackInfo(address item) external view returns (AttackInfo memory info); function itemInfo(address item, uint itemId) external view returns (ItemInfo memory info); function equippedOn(address item, uint itemId) external view returns (address hero, uint heroId); function itemAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids); function consumableAttributes(address item) external view returns (int32[] memory values, uint8[] memory ids); function consumableStats(address item) external view returns (IStatController.ChangeableStats memory stats); function casterAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids); function targetAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids); function itemAttackInfo(address item, uint itemId) external view returns (AttackInfo memory info); function score(address item, uint tokenId) external view returns (uint); function isAllowedToTransfer(address item, uint tokenId) external view returns (bool); // --- function mint(address item, address recipient) external returns (uint itemId); function reduceDurability(address hero, uint heroId, uint8 biome, bool reduceDurabilityAllSkills) external; function destroy(address item, uint tokenId) external; function takeOffDirectly( address item, uint itemId, address hero, uint heroId, uint8 itemSlot, address destination, bool broken ) external; /// @notice SIP-003: item fragility counter that displays the chance of an unsuccessful repair. /// @dev [0...100%], decimals 3, so the value is in the range [0...10_000] function itemFragility(address item, uint itemId) external view returns (uint); /// @notice SIP-003: The quest mechanic that previously burned the item will increase its fragility by 1% function incBrokenItemFragility(address item, uint itemId) external; function equip( address hero, uint heroId, address[] calldata items, uint[] calldata itemIds, uint8[] calldata itemSlots ) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IMinter { function amountForDungeon(uint dungeonBiomeLevel, uint heroLevel) external view returns (uint); function mintDungeonReward(uint64 dungeonId, uint dungeonBiomeLevel, uint heroLevel) external returns (uint amount); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IOracle { function getRandomNumber(uint max, uint seed) external returns (uint); function getRandomNumberInRange(uint min, uint max, uint seed) external returns (uint); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; import "./IFightCalculator.sol"; interface IPvpController { enum PvpParams { NONE_0, /// @notice Hero can be pvp-staked if his level is greater of equal to the given min level MIN_HERO_LEVEL_1, /// @notice Address of IGuildStakingAdapter, can be not initialized GUILD_STAKING_ADAPTER_2, /// @notice Unique ID of the pvp-fight (each pvp-fight consists from multiple turns) FIGHT_COUNTER_3 // max 255 params because enum is uint8 by default } /// @custom:storage-location erc7201:pvp.controller.main struct MainState { /// @notice Mapping to store various params of PvpController mapping(PvpParams param => uint value) pvpParam; /// @notice Current states of biomes mapping(uint8 biome => BiomeData) biomeState; /// @notice Biomes owned by the guilds mapping(uint guildId => uint8 biome) ownedBiome; mapping(uint epochWeek => EpochData) epochData; } struct EpochData { /// @notice Current state of the user in the current epoch mapping (address user => PvpUserState) pvpUserState; /// @notice biome data for the given epoch mapping(uint8 biome => EpochBiomeData) epochBiomeData; /// @notice All prepared pvp-fights for the given user /// Index of currently active fight is stored in {pvpUserState.activeFightIndex1} mapping (address user => PvpFightData[]) fightData; /// @notice All currently registered packed-heroes EnumerableSet.UintSet stakedHeroes; /// @notice Weekly request of the guild to dominate at the given biome starting from the next week mapping(uint guildId => uint8 biome) targetBiome; /// @notice All guilds pretend for the given biome mapping(uint8 biome => EnumerableSet.UintSet guildIds) biomeGuilds; } /// @notice Current state of the user. Possible states: user has or hasn't staked a hero in pvp. /// Each user is able to stake pvp-heroes multiple times per epoch /// but the user is able to stake only 1 pvp-hero at any moment. /// @dev Implementation assumes that the struct occupies single slot, the struct is read as a whole struct PvpUserState { /// @notice Domination biome at the moment of staking /// @dev not 0 if the user has pvp-staked hero uint8 biome; /// @notice 1-based index of currently active fight in {fightData} (the fight is either prepared or in-progress). /// 0 - there is no active fight uint32 activeFightIndex1; /// @notice How many times the user has staked heroes for PVP /// @dev Max possible value is limited by MAX_NUMBER_STAKES_FOR_USER_PER_EPOCH uint32 numHeroesStaked; /// @notice User's guild at the moment of staking /// 0 if user has no hero staked in pvp currently uint64 guildId; /// @notice Total number of pvp-fights performed since the last call of addPvpHero. /// @dev All pvp-fights are won here because looser is auto removed. uint8 countFights; /// @notice Max number of pvp-fights allowed by the user per single call of addPvpHero, 0 - no limits uint8 maxFights; /// @notice Unique id of the current pvp-fight (the fight with activeFightIndex1) uint48 fightId; } struct BiomeData { /// @notice Biome owner - the guild that dominates in the biome at the given epoch. He has a right to get a tax /// @dev Assume here that uint64 is enough to store any guildId. It allows us to store whole struct in a single slot uint64 guildBiomeOwnerId; /// @notice Current epoch (last epoch for which pvp-battle was made) /// 0 if epoch was never started uint32 startedEpochWeek; /// @notice Number of consecutive epochs during which {guildBiomeOwnerId} wasn't changed uint16 dominationCounter; } struct EpochBiomeData { /// @notice List of guilds asked for domination in the biome => total points scored by the guilds in the given epoch /// @dev guildId => count points EnumerableMap.UintToUintMap guildPoints; /// @notice All users free for pvp-fight /// User is added here on registration and removed as soon as the fight for the user is initialized. mapping(uint guildId => EnumerableSet.AddressSet) freeUsers; /// @notice All users (from the {guilds}) provided heroes for pvp /// @dev guildId => (user address => packedHero (hero + heroId)) mapping(uint guildId => EnumerableMap.AddressToUintMap) registeredHeroes; /// @notice The skills and attack type selected in advance mapping(bytes32 packedHero => bytes) pvpStrategy; } enum PvpFightStatus { /// @notice No fight, the hero doesn't have selected opponent NOT_INITIALIZED_0, /// @notice The hero has opponent, the fight is not started PREPARED_1, /// @notice The fight is started but not completed FIGHTING_2, /// @notice The fight is completed, the hero is the winner WINNER_3, /// @notice The fight is completed, the hero is the looser LOSER_4 } /// @notice Current state of the fight /// @dev Implementation assumes that the struct occupies single slot, the struct is read as a whole /// @dev We don't store biome and guildId here. This info is stored in user state and can be lost after fight ending. struct PvpFightData { /// @notice address of user whose hero is the fight opponent address fightOpponent; /// @notice Current status of PVP-fight PvpFightStatus fightStatus; /// @notice Current value of the health (only when fightStatus is FIGHTING_2) uint32 health; /// @notice Current value of the mana (only when fightStatus is FIGHTING_2) uint32 mana; /// @notice Number of moves made (only when fightStatus is FIGHTING_2) uint8 countTurns; } /// @dev Implementation assumes that the struct occupies single slot, the struct is read as a whole struct PvpFightResults { bool completed; uint8 totalCountFights; uint32 healthHero; uint32 healthOpponent; uint32 manaConsumedHero; uint32 manaConsumedOpponent; } /// @notice Strategy how to use attack info enum PvpBehaviourStrategyKinds { /// @notice Use all skills, use magic attack if it's available /// @dev {PvpStrategyDefault} is used as data in {addPvpHero} DEFAULT_STRATEGY_0 // new strategies are able to use different structures to store data passed to {addPvpHero} } /// @notice The data provided by user at the staking with {DEFAULT_STRATEGY_0} struct PvpStrategyDefault { /// @notice Should be equal to DEFAULT_STRATEGY_0 uint behaviourStrategyKind; IFightCalculator.AttackInfo attackInfo; } struct HeroData { address hero; uint heroId; bytes pvpStrategy; } /// ------------------------------------------------------------------------------------------------------------------ /// ------------------------------------------------------------------------------------------------------------------ /// ------------------------------------------------------------------------------------------------------------------ /// @notice Update epoch if necessary and return actual biome owner and tax /// @return guildId Owner of the biome /// @return taxPercent Tax percent , [0...100_000], decimals 3 function refreshBiomeTax(uint8 biome) external returns (uint guildId, uint taxPercent); function isHeroStakedCurrently(address hero, uint heroId) external view returns (bool staked); function onGuildDeletion(uint guildId) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IStatController.sol"; import "../openzeppelin/EnumerableMap.sol"; /// @notice Terms /// Reinforcement v1: helper is selected randomly in askHero, fixed part of rewards (tokens and NFT) is sent to the helper. /// Guild reinforcement: helper is selected from guild heroes. Rewards are sent to guild bank. /// Reinforcement v2: helper is selected manually in askHeroV2, helper receives fixed amount. interface IReinforcementController { enum ConfigParams { /// @notice Packed MinMaxBoardV2 V2_MIN_MAX_BOARD_0 } /// @custom:storage-location erc7201:reinforcement.controller.main struct MainState { // ------------------------ Reinforcement v1 /// @dev minLvl8 + minLifeChances8 bytes32 config; /// @dev hero token + hero id => heroInfo(biome8 + score128 + fee8 + stakeTs64) mapping(bytes32 => bytes32) _stakedHeroes; /// @dev biome => helperAdr+id mapping(uint => EnumerableSet.Bytes32Set) _internalIdsByBiomes; /// @dev biome => score // The field is deprecated and not updated any more mapping(uint => uint) maxScore; /// @dev heroAdr+id => itemAdr+id mapping(bytes32 => bytes32[]) _heroNftRewards; /// @dev heroAdr+id => tokenAdr and amount map mapping(bytes32 => EnumerableMap.AddressToUintMap) _heroTokenRewards; // ------------------------ Guild reinforcement /// @notice All staked guild heroes for the given guild /// @dev helper (hero token + hero id) => guild mapping(bytes32 packedHero => uint guildId) stakedGuildHeroes; /// @notice All guild heroes that are currently in use by guild reinforcement /// It's allowed to withdraw a hero before reinforcement releasing, /// so it's possible to have !0 in {guildBusyHelpers} and 0 in {stakedGuildHeroes} simultaneously. /// @dev helper (hero token + hero id) => guildId (guild at the moment of askGuildReinforcement) mapping(bytes32 packedHero => uint guildId) busyGuildHelpers; /// @notice All (free and busy) staked guild heroes per guild. /// guild => (packed helper => guild where the helper is busy currently) /// @dev There is a chance that guilds are different here /// i.e. hero can be: /// 1) added to G1 2) staked in G1 3) asked for help 4) withdrawn 5) G1=>G2 6) staked in G2 /// In such case guildHelpers[G2][hero] = G1, guildHelpers[G1][hero] = 0 /// After releasing guildHelpers[G2][hero] = 0 mapping(uint guildId => EnumerableMap.Bytes32ToUintMap) guildHelpers; /// @notice Moment of withdrawing the hero from staking. Next staking is possible in 1 day since withdrawing mapping(bytes32 packedHero => uint lastWithdrawTimestamp) lastGuildHeroWithdrawTs; // ------------------------ Reinforcement v2 /// @notice Map to store various config params mapping(ConfigParams paramId => uint) configParams; mapping(bytes32 packedHero => HeroInfoV2) stakedHeroesV2; /// @notice biome => set of packedHero. All staked heroes (they can be busy of free currently) mapping(uint biome => EnumerableSet.Bytes32Set) heroesByBiomeV2; mapping(uint biome => LastWindowsV2) stat24hV2; } /// @notice Deprecated. Reinforcement v1 struct HeroInfo { uint8 biome; uint score; // stored in 128 but easy to use 256 /// @notice To helper ratio uint8 fee; uint64 stakeTs; } struct HeroInfoV2 { uint8 biome; uint64 stakeTs; /// @notice Amount of game token that is paid to the helper at the moment of the call {askHeroV2} uint128 rewardAmount; } /// @notice Statistic of askHeroV2 calls per last 24 hours at the moment of the last call struct LastWindowsV2 { /// @notice 24 hours are divided on 8 intervals, each interval is 3 hour /// Current basket has index {basketIndex} /// {baskets[current basket]} contains "old" value. /// New value for the current basket is collected in {basketValue}. /// The value for the current basket is calculated as weighted average of old and new values. /// New value replaces the old value at the moment of changing current basket index. uint24[8] baskets; /// @notice New value (hits counter) for current basket uint24 basketValue; /// @notice Abs. index of the current basket (abs. hour / 3) uint48 basketIndex; } /// @dev 1 slot struct ConfigReinforcementV2 { /// @notice if Number-of-askHeroV2-calls is below given value then burn fee has min value uint32 minNumberHits; /// @notice if Number-of-askHeroV2-calls is above given value then burn fee has max value uint32 maxNumberHits; /// @notice Lowest fee = amountForDungeon / given value, i.e. 100 => amountForDungeon/100 as lower fee uint32 lowDivider; /// @notice Highest fee = amountForDungeon / given value, i.e. 2 => amountForDungeon/2 as highest fee uint32 highDivider; /// @notice Limit for min level of the staked hero /// In practice we need following limitation: (stats.level < 5 || (stats.level - 5) / 5 < biome) /// so, levelLimit should be equal 5 /// In tests we need to be able to disable such limitation, so levelLimit = 0 allow to disable that constraint uint8 levelLimit; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function toHelperRatio(address heroToken, uint heroId) external view returns (uint); function isStaked(address heroToken, uint heroId) external view returns (bool); function registerTokenReward(address heroToken, uint heroId, address token, uint amount) external; function registerNftReward(address heroToken, uint heroId, address token, uint tokenId) external; function askHeroV2(address hero, uint heroId, address helper, uint helperId) external returns (int32[] memory attributes); function askGuildHero(address hero, uint heroId, address helper, uint helperId) external returns (int32[] memory attributes); /// @notice Return the guild in which the hero is currently asked for guild reinforcement function busyGuildHelperOf(address heroToken, uint heroId) external view returns (uint guildId); function releaseGuildHero(address helperHeroToken, uint helperHeroTokenId) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; interface IRewardsPool { /// @custom:storage-location erc7201:rewards.pool.main struct MainState { mapping(address token => uint baseAmountValue) baseAmounts; } function balanceOfToken(address token) external view returns (uint); function rewardAmount(address token, uint maxBiome, uint maxNgLevel, uint biome, uint heroNgLevel) external view returns (uint); function sendReward(address token, uint rewardAmount_, address receiver) external; function lostProfitPercent(uint maxBiome, uint maxNgLevel, uint heroNgLevel) external view returns (uint percent); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; interface IShelterAuction { enum ShelterAuctionParams { NONE_0, POSITION_COUNTER_1, BID_COUNTER_2, FEE_3 // max 255 params because enum is uint8 by default } //region ------------------------ Data types /// @custom:storage-location erc7201:shelter.auction.main struct MainState { /// @notice Mapping to store auction params (i.e. counters) mapping(ShelterAuctionParams param => uint value) params; /// @notice Hold all positions. Any record should not be removed mapping(uint positionId => Position) positions; /// @dev BidId => Bid. Hold all bids. Any record should not be removed mapping(uint bidId => AuctionBid) auctionBids; /// @notice List of currently opened positions EnumerableSet.UintSet openPositions; /// @notice Seller to position map /// At any moment each guild can have only one opened position to sell mapping(uint sellerGuildId => uint openedPositionId) sellerPosition; /// @notice Position that the buyer is going to purchase. /// At any moment each guild can have only one opened position to purchase mapping(uint buyerGuildId => BuyerPositionData) buyerPosition; /// @notice All open and close bids for the given position mapping(uint positionId => uint[] bidIds) positionToBidIds; /// @notice Timestamp of the last bid for the auction mapping(uint positionId => uint timestamp) lastAuctionBidTs; } struct Position { bool open; /// @notice User that opens the position. The user belongs to the guild with id = {sellerGuildId} address seller; /// @notice Assume that shelter can be stored as uint64 uint64 shelterId; uint128 positionId; /// @notice Min allowed (initial) auction price. Only first bid is able to use it. uint128 minAuctionPrice; uint128 sellerGuildId; } struct AuctionBid { /// @notice Only last bid is opened, all previous bids are closed automatically bool open; /// @notice User that opens the bid. The user belongs to the guild with id = {buyerGuildId} address buyer; uint128 bidId; uint128 positionId; /// @notice Bid amount in terms of game token. This amount is transferred from guild Bank to ShelterAuction balance uint128 amount; uint128 buyerGuildId; } struct BuyerPositionData { /// @notice ID of the position that the buyer is going to purchase uint128 positionId; /// @notice 0-based index of the opened bid in {positionToBidIds} uint128 bidIndex; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function positionBySeller(uint sellerGuildId_) external view returns (uint positionId); function positionByBuyer(uint buyerGuildId) external view returns (uint positionId, uint bidIndex); function posByShelter(uint shelterId_) external view returns (uint positionId); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; interface IShelterController { /// @custom:storage-location erc7201:shelter.controller.main struct MainState { /// @notice List of items allowed to be purchased in the shelter mapping(uint shelterId => EnumerableSet.AddressSet) shelterItems; /// @notice Data of items available for purchasing in the given shelter mapping(uint shelterId => mapping(address item => ShelterItemData)) shelterItemData; // @notice Statistics how much items were purchased per day mapping(uint shelterId => mapping(uint32 epochDay => mapping(address item => uint))) countPurchasedItems; /// @notice List of registered shelters in {biome} mapping(uint biome => EnumerableSet.UintSet shelterUids) shelters; /// @notice Initial price of the shelters in game tokens mapping(uint shelterId => uint) shelterPrices; /// @notice Shelters belong to a specific guild (not the player) /// Shelters can be free (don't belong to any guild) mapping(uint shelterId => uint guildId) shelterToGuild; /// @notice Each guild can own 0 or 1 shelter mapping(uint guildId => uint shelterId) guildToShelter; } struct ShelterItemData { /// @notice Price of the item in pvp-points uint64 priceInPvpPoints; /// @notice Price of the item game token uint128 priceInGameToken; /// @notice Max number of items that can be purchases per day in the shelter. 0 - no limitations uint16 maxItemsPerDayLimit; } /// ---------------------------------------------------------------------------------------------- function clearShelter(uint guildId) external; function guildToShelter(uint guildId) external view returns (uint shelterId); function changeShelterOwner(uint shelterId, uint newOwnerGuildId) external; function shelterToGuild(uint shelterId) external view returns (uint guildId); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; import "../openzeppelin/EnumerableMap.sol"; interface IStatController { /// @custom:storage-location erc7201:stat.controller.main struct MainState { mapping(bytes32 => bytes32[]) heroTotalAttributes; /// @dev heroAdr+heroId => int32 packed strength, dexterity, vitality, energy mapping(bytes32 => bytes32) _heroCore; mapping(bytes32 => bytes32[]) heroBonusAttributes; mapping(bytes32 => bytes32[]) heroTemporallyAttributes; /// @dev heroAdr+heroId => uint32 packed level, experience, life, mana, lifeChances mapping(bytes32 => bytes32) heroStats; /// @dev heroAdr+heroId+itemSlot => itemAdr + itemId mapping(bytes32 => bytes32) heroSlots; /// @dev heroAdr+heroId => busy slots uint8[] packed mapping(bytes32 => bytes32) heroBusySlots; mapping(bytes32 => EnumerableSet.AddressSet) usedConsumables; /// @dev heroCustomDataV2 is used instead mapping(bytes32 => mapping(bytes32 => uint)) _deprecated_heroCustomData; mapping(bytes32 => uint) globalCustomData; /// @notice packNftIdWithValue(hero, heroId, ngLevel) => hero custom data map /// @dev initially it was packedHero => hero custom data map mapping(bytes32 => EnumerableMap.Bytes32ToUintMap) heroCustomDataV2; } enum ATTRIBUTES { // core STRENGTH, // 0 DEXTERITY, // 1 VITALITY, // 2 ENERGY, // 3 // attributes DAMAGE_MIN, // 4 DAMAGE_MAX, // 5 ATTACK_RATING, // 6 DEFENSE, // 7 BLOCK_RATING, // 8 LIFE, // 9 MANA, // 10 // resistance FIRE_RESISTANCE, // 11 COLD_RESISTANCE, // 12 LIGHTNING_RESISTANCE, // 13 // dmg against DMG_AGAINST_HUMAN, // 14 DMG_AGAINST_UNDEAD, // 15 DMG_AGAINST_DAEMON, // 16 DMG_AGAINST_BEAST, // 17 // defence against DEF_AGAINST_HUMAN, // 18 DEF_AGAINST_UNDEAD, // 19 DEF_AGAINST_DAEMON, // 20 DEF_AGAINST_BEAST, // 21 // --- unique, not augmentable // hero will not die until have positive chances LIFE_CHANCES, // 22 // increase chance to get an item MAGIC_FIND, // 23 // decrease chance to get an item DESTROY_ITEMS, // 24 // percent of chance x2 dmg CRITICAL_HIT, // 25 // dmg factors MELEE_DMG_FACTOR, // 26 FIRE_DMG_FACTOR, // 27 COLD_DMG_FACTOR, // 28 LIGHTNING_DMG_FACTOR, // 29 // increase attack rating on given percent AR_FACTOR, // 30 // percent of damage will be converted to HP LIFE_STOLEN_PER_HIT, // 31 // amount of mana restored after each battle MANA_AFTER_KILL, // 32 // reduce all damage on percent after all other reductions DAMAGE_REDUCTION, // 33 // -- statuses // chance to stun an enemy, stunned enemy skip next hit STUN, // 34 // chance burn an enemy, burned enemy will loss 50% of defence BURN, // 35 // chance freeze an enemy, frozen enemy will loss 50% of MELEE damage FREEZE, // 36 // chance to reduce enemy's attack rating on 50% CONFUSE, // 37 // chance curse an enemy, cursed enemy will loss 50% of resistance CURSE, // 38 // percent of dmg return to attacker REFLECT_DAMAGE_MELEE, // 39 REFLECT_DAMAGE_MAGIC, // 40 // chance to poison enemy, poisoned enemy will loss 10% of the current health POISON, // 41 // reduce chance get any of uniq statuses RESIST_TO_STATUSES, // 42 END_SLOT // 43 } // possible // HEAL_FACTOR struct CoreAttributes { int32 strength; int32 dexterity; int32 vitality; int32 energy; } struct ChangeableStats { uint32 level; uint32 experience; uint32 life; uint32 mana; uint32 lifeChances; } enum ItemSlots { UNKNOWN, // 0 HEAD, // 1 BODY, // 2 GLOVES, // 3 BELT, // 4 AMULET, // 5 BOOTS, // 6 RIGHT_RING, // 7 LEFT_RING, // 8 RIGHT_HAND, // 9 LEFT_HAND, // 10 TWO_HAND, // 11 SKILL_1, // 12 SKILL_2, // 13 SKILL_3, // 14 END_SLOT // 15 } struct NftItem { address token; uint tokenId; } enum Race { UNKNOWN, // 0 HUMAN, // 1 UNDEAD, // 2 DAEMON, // 3 BEAST, // 4 END_SLOT // 5 } struct ChangeAttributesInfo { address heroToken; uint heroTokenId; int32[] changeAttributes; bool add; bool temporally; } struct BuffInfo { address heroToken; uint heroTokenId; uint32 heroLevel; address[] buffTokens; uint[] buffTokenIds; } /// @dev This struct is used inside event, so it's moved here from lib struct ActionInternalInfo { int32[] posAttributes; int32[] negAttributes; uint32 experience; int32 heal; int32 manaRegen; int32 lifeChancesRecovered; int32 damage; int32 manaConsumed; address[] mintedItems; } function initNewHero(address token, uint tokenId, uint heroClass) external; function heroAttributes(address token, uint tokenId) external view returns (int32[] memory); function heroAttribute(address token, uint tokenId, uint index) external view returns (int32); function heroAttributesLength(address token, uint tokenId) external view returns (uint); function heroBaseAttributes(address token, uint tokenId) external view returns (CoreAttributes memory); function heroCustomData(address token, uint tokenId, bytes32 index) external view returns (uint); function globalCustomData(bytes32 index) external view returns (uint); function heroStats(address token, uint tokenId) external view returns (ChangeableStats memory); function heroItemSlot(address token, uint64 tokenId, uint8 itemSlot) external view returns (bytes32 nftPacked); function heroItemSlots(address heroToken, uint heroTokenId) external view returns (uint8[] memory); function isHeroAlive(address heroToken, uint heroTokenId) external view returns (bool); function levelUp(address token, uint tokenId, uint heroClass, CoreAttributes memory change) external returns (uint newLvl); function changeHeroItemSlot( address heroToken, uint64 heroTokenId, uint itemType, uint8 itemSlot, address itemToken, uint itemTokenId, bool equip ) external; function changeCurrentStats( address token, uint tokenId, ChangeableStats memory change, bool increase ) external; function changeBonusAttributes(ChangeAttributesInfo memory info) external; function registerConsumableUsage(address heroToken, uint heroTokenId, address item) external; function clearUsedConsumables(address heroToken, uint heroTokenId) external; function clearTemporallyAttributes(address heroToken, uint heroTokenId) external; function buffHero(BuffInfo memory info) external view returns (int32[] memory attributes, int32 manaConsumed); function setHeroCustomData(address token, uint tokenId, bytes32 index, uint value) external; function setGlobalCustomData(bytes32 index, uint value) external; /// @notice Restore life and mana during reinforcement /// @dev Life and mana will be increased on ((current life/mana attr value) - (prev life/mana attr value)) /// @param prevAttributes Hero attributes before reinforcement function restoreLifeAndMana(address heroToken, uint heroTokenId, int32[] memory prevAttributes) external; function reborn(address heroToken, uint heroTokenId, uint heroClass) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IGOC.sol"; import "../interfaces/IItemController.sol"; import "../interfaces/IStatController.sol"; import "../lib/ControllerContextLib.sol"; import "../openzeppelin/EnumerableSet.sol"; import "./IController.sol"; import "./IHeroController.sol"; import "./IOracle.sol"; interface IStoryController { enum AnswerResultId { UNKNOWN, // 0 SUCCESS, // 1 ATTRIBUTE_FAIL, // 2 RANDOM_FAIL, // 3 DELAY_FAIL, // 4 HERO_CUSTOM_DATA_FAIL, // 5 GLOBAL_CUSTOM_DATA_FAIL, // 6 END_SLOT } enum CustomDataResult { UNKNOWN, // 0 HERO_SUCCESS, // 1 HERO_FAIL, // 2 GLOBAL_SUCCESS, // 3 GLOBAL_FAIL, // 4 END_SLOT } /// @custom:storage-location erc7201:story.controller.main struct MainState { // --- STORY REG INFO --- /// @dev Uniq story identification. mapping(uint32 => uint16) storyIds; /// @dev Revers mapping for stories for using in the next object rewrite logic. mapping(uint16 => uint32) idToStory; /// @dev Store used ids for stories. mapping(uint16 => bool) _usedStoryIds; /// @dev Prevent register the story twice mapping(uint32 => bool) registeredStories; // --- ANSWER MAPPING --- /// @dev storyId => all story pages. We need to have this mapping for properly remove meta info mapping(uint16 => EnumerableSet.UintSet) allStoryPages; /// @dev storyId => all possible answers. We need to have this mapping for properly remove meta info mapping(uint16 => EnumerableSet.Bytes32Set) allStoryAnswers; /// @dev storyId + pageId + heroClass (zero is default answers) => storyId + pageId + heroClass (zero is default answers) + answerId mapping(bytes32 => bytes32[]) answers; /// @dev answerUnPackedId + answerResultId => nextPageIds (will be chosen randomly from this array) /// where answerResultId is: /// 0 - unknown, /// 1 - success, /// 2 - attr fail /// 3 - random fail /// 4 - delay fail /// 5 - hero custom data fail /// 6 - global custom data fail /// see COUNT_ANSWER_RESULT_IDS mapping(bytes32 => uint16[]) nextPageIds; /// @dev story + pageId + heroClass (zero is default answers) => random nextObjs (adr + id, like packed nft id) mapping(bytes32 => uint32[]) nextObjectsRewrite; /// @dev answerPackedId => packed array of uint32[] /// 0 - random requirement(uint32, 1 - 99% success of this action, zero means no check) /// 1 - delay requirement(uint32, if time since the last call more than this value the check is fail, zero means no check) /// 2 - isFinalAnswer(uint8) mapping(bytes32 => bytes32) answerAttributes; // --- ANSWER REQUIREMENTS --- /// @dev answerPackedId => array of AttributeRequirementsPacked mapping(bytes32 => bytes32[]) attributeRequirements; /// @dev answerPackedId=> array of ItemRequirementsPacked mapping(bytes32 => bytes32[]) itemRequirements; /// @dev answerPackedId => array of TokenRequirementsPacked mapping(bytes32 => bytes32[]) tokenRequirements; /// @dev answerPackedId => custom data for hero mapping(bytes32 => CustomDataRequirementPacked[]) heroCustomDataRequirement; /// @dev answerPackedId => global custom data mapping(bytes32 => CustomDataRequirementPacked[]) globalCustomDataRequirement; // --- ANSWER RESULTS --- /// @dev answerPackedId => change attributes mapping(bytes32 => bytes32[]) successInfoAttributes; /// @dev answerPackedId => change stats mapping(bytes32 => bytes32) successInfoStats; /// @dev answerPackedId => mint items mapping(bytes32 => bytes32[]) successInfoMintItems; /// @dev answerPackedId => change attributes mapping(bytes32 => bytes32[]) failInfoAttributes; /// @dev answerPackedId => change stats mapping(bytes32 => bytes32) failInfoStats; /// @dev answerPackedId => mint items mapping(bytes32 => bytes32[]) failInfoMintItems; /// @dev answerUnPackedId + CustomDataResult => custom data array change /// where CustomDataResult is /// 1 - hero success /// 2 - hero fail /// 3 - global success /// 4 - global fail /// see COUNT_CUSTOM_DATA_RESULT_IDS mapping(bytes32 => bytes32[]) customDataResult; /// @notice answerPackedId => slot+chance+stopIfBurnt /// @dev Since SIP-003 the items are not burn but broke mapping(bytes32 => bytes32[]) burnItem; // --- GENERAL STORY REQUIREMENTS --- /// @dev story => Custom hero data requirements for a story. If exist and hero is not eligible should be not chose in a dungeon. mapping(uint => CustomDataRequirementRangePacked[]) storyRequiredHeroData; /// @dev story => Minimal level for the history. 0 means no requirements. mapping(uint => uint) storyRequiredLevel; // --- HERO STATES --- /// @dev hero + heroId + storyId => pageId + heroLastActionTS mapping(bytes32 => bytes32) heroState; // --- OTHER --- /// @dev storyId => build hash for the last update mapping(uint16 => uint) storyBuildHash; /// @notice Number of already minted items by the user within the given iteration of the story. /// Only minting of the given number of items is allowed per iteration (see MAX_MINTED_ITEMS_PER_ITERATION). /// @dev hero, heroId, story => mintedInIteration /// This map is not cleared: storyId:objectId is 1:1, each object has own sequence of iterations without duplicates mapping(bytes32 => mapping(uint iteration => uint countMintedItems)) mintedInIteration; /// @notice True if the story is allowed to be skipped, see SCR-1248 EnumerableSet.UintSet skippableStory; } /// @dev We need to have flat structure coz Solidity can not handle arrays of structs properly struct StoryMetaInfo { uint16 storyId; // --- story reqs bytes32[] requiredCustomDataIndex; uint64[] requiredCustomDataMinValue; uint64[] requiredCustomDataMaxValue; bool[] requiredCustomDataIsHero; uint minLevel; // --- answer reqs AnswersMeta answersMeta; AnswerNextPageMeta answerNextPage; AnswerAttributeRequirementsMeta answerAttributeRequirements; AnswerItemRequirementsMeta answerItemRequirements; AnswerTokenRequirementsMeta answerTokenRequirements; AnswerAttributesMeta answerAttributes; AnswerCustomDataMeta answerHeroCustomDataRequirement; AnswerCustomDataMeta answerGlobalCustomDataRequirement; // --- answer results AnswerBurnRandomItemMeta answerBurnRandomItemMeta; NextObjRewriteMeta nextObjRewriteMeta; // --- story results AnswerResultMeta successInfo; AnswerResultMeta failInfo; AnswerCustomDataResultMeta successHeroCustomData; AnswerCustomDataResultMeta failHeroCustomData; AnswerCustomDataResultMeta successGlobalCustomData; AnswerCustomDataResultMeta failGlobalCustomData; } struct NextObjRewriteMeta { uint16[] nextObjPageIds; uint8[] nextObjHeroClasses; uint32[][] nextObjIds; } struct AnswersMeta { uint16[] answerPageIds; uint8[] answerHeroClasses; uint16[] answerIds; } struct AnswerNextPageMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; uint8[] answerResultIds; uint16[][] answerNextPageIds; } struct AnswerAttributeRequirementsMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; bool[][] cores; uint8[][] ids; int32[][] values; } struct AnswerItemRequirementsMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; address[][] requireItems; bool[][] requireItemBurn; bool[][] requireItemEquipped; } struct AnswerTokenRequirementsMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; address[][] requireToken; uint88[][] requireAmount; bool[][] requireTransfer; } struct AnswerAttributesMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; uint32[] randomRequirements; uint32[] delayRequirements; bool[] isFinalAnswer; } struct AnswerCustomDataMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; bytes32[][] dataIndexes; bool[][] mandatory; uint64[][] dataValuesMin; uint64[][] dataValuesMax; } struct AnswerResultMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; uint8[][] attributeIds; /// @dev Max value is limitied by int24, see toBytes32ArrayWithIds impl int32[][] attributeValues; uint32[] experience; int32[] heal; int32[] manaRegen; int32[] lifeChancesRecovered; int32[] damage; int32[] manaConsumed; address[][] mintItems; uint32[][] mintItemsChances; } struct AnswerCustomDataResultMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; bytes32[][] dataIndexes; int16[][] dataValues; } struct AnswerBurnRandomItemMeta { uint16[] pageId; uint8[] heroClass; uint16[] answerId; /// @notice 0 - random slot uint8[][] slots; /// @notice typical chances are [0..100] (no decimals here) uint64[][] chances; /// @notice Since SIP-003 the burning is replaced by breaking bu the name is kept as is bool[][] isStopIfBurnt; } struct CustomDataRequirementPacked { bytes32 index; /// @dev min(uint64) + max(uint64) + mandatory(uint8) bytes32 data; } struct CustomDataRequirementRangePacked { bytes32 index; /// @dev min(uint64) + max(uint64) + isHeroData(uint8) bytes32 data; } struct StatsChange { uint32 experience; int32 heal; int32 manaRegen; int32 lifeChancesRecovered; int32 damage; int32 manaConsumed; } struct StoryActionContext { uint stageId; uint iteration; bytes32 answerIdHash; bytes32 answerAttributes; address sender; address heroToken; IController controller; IOracle oracle; uint8 heroClassFromAnswerHash; uint8 biome; uint16 storyId; uint16 storyIdFromAnswerHash; uint16 pageIdFromAnswerHash; uint16 answerNumber; uint16 pageId; uint32 objectId; uint64 dungeonId; uint40 heroLastActionTS; uint80 heroTokenId; IStatController.ChangeableStats heroStats; } // --- WRITE --- function storyAction( address sender, uint64 dungeonId, uint32 objectId, uint stageId, address heroToken, uint heroTokenId, uint8 biome, uint iteration, bytes memory data ) external returns (IGOC.ActionResult memory); // --- READ --- function isStoryAvailableForHero(uint32 objectId, address heroToken, uint heroTokenId) external view returns (bool); function idToStory(uint16 id) external view returns (uint32 objectId); function heroPage(address hero, uint80 heroId, uint16 storyId) external view returns (uint16 pageId); function storyIds(uint32 objectId) external view returns (uint16); function registeredStories(uint32 objectId) external view returns (bool); function skippableStory(uint16 storyId) external view returns (bool); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "./IItemController.sol"; interface ITreasury { function balanceOfToken(address token) external view returns (uint); function sendToDungeon(address dungeon, address token, uint amount) external; }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/EnumerableSet.sol"; interface IUserController { //region ------------------------ Data types enum LootBoxKind { /// @notice small loot box - reward for the daily activity DAILY_0, /// @notice large loot box - reward for the weekly activity (daily activity is passed each ot of the 7 days) WEEKLY_1, END_SLOT } /// @dev registerPassedDungeon assumes that the whole struct takes single slot only, not more struct UserActivity { /// @notice A day for which the daily activity is calculated (see counterXXX below) /// The number of days since 1970-01-01 uint32 epochDay; /// @notice A week for which total count of daily activities were calculated /// The number of weeks since (1970-01-01 Thursday) - 3 days = (1969-12-29 Monday) uint32 epochWeek; /// @notice Count of dungeons passed during the day uint32 counterPassedDungeons; /// @notice Count of PvP during the day uint32 counterPvp; /// @notice Count of daily activities completed per the week uint16 dailyActivities; /// @notice Daily activity is completed and small loot box is added to the earned loot boxes bool dailyLootBoxReceived; /// @notice Weekly activity is completed and large loot box is added to the earned loot boxes bool weeklyLootBoxReceived; } struct EarnedLootBoxes { /// @notice Count of loot boxes earned by daily activity uint32 dailyCounter; /// @notice Count of loot boxes earned by weekly activity uint32 weeklyCounter; } struct LootBoxConfig { address[] mintItems; uint32[] mintItemsChances; uint maxDropItems; } enum UserControllerParam { /// @notice Price of story skipping in game points PRICE_STORY_SKIPPING_1 } /// @custom:storage-location erc7201:user.controller.main struct MainState { /// @notice Amount of sacra required to rename user account uint feeRenaming; /// @dev user EOA => account name mapping(address => string) userAccountName; /// @dev name => user EOA, needs for checking uniq names mapping(string => address) nameToUserAccount; /// @notice user => daily activity info mapping(address => UserActivity) userActivity; /// @notice user => earned loot boxes mapping(address => EarnedLootBoxes) counterLootBoxes; /// @notice Configs of loot boxes of various kinds mapping(LootBoxKind => LootBoxConfig) lootBoxConfig; /// @dev Deprecated, controller is used instead. address userTokensVault; /// @dev user EOA => account avatar mapping(address => string) userAvatar; // @notice Hall of Fame: ngLevel [1...99] => who opened the NG_LEVEL first mapping(uint8 ngLevel => FameHallData) fameHall; /// @notice Points earned for passing dungeons mapping(address user => uint gamePoints) gamePoints; /// @notice List of objects (currently only stories) passed by the given account /// @dev hashes of the stories are as encodePacked("STORY_{ID}") mapping(address user => EnumerableSet.Bytes32Set hashes) passedObjects; /// @notice Values of various params, see {UserControllerParam} mapping(UserControllerParam paramId => uint value) userControllerParams; } struct FameHallData { // ------------ slot 1 /// @notice The hero who opened given the NG_LEVEL first address hero; uint64 heroId; // ------------ slot 2 /// @notice The owner of the hero address heroOwner; /// @notice Timestamp of the moment of the opening given NG_LEVEL uint64 tsOpen; } //endregion ------------------------ Data types /// @notice Register daily activity - a dungeon was passed /// @param user Owner of the hero who has passed the dungeon function registerPassedDungeon(address user) external; /// @notice Register daily activity - PvP was made /// @param user Owner of the hero who has taken participation in the PvP function registerPvP(address user, bool isWinner) external; function registerFameHallHero(address hero, uint heroId, uint8 openedNgLevel) external; function useGamePointsToSkipStore(address user, uint16 storyId) external; function setStoryPassed(address user, uint16 storyId) external; function isStoryPassed(address user, uint16 storyId) external view returns (bool); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IERC20.sol"; /// @notice Common internal utils, shared constants library AppLib { /// @notice Biome owner has the right to receive 1% tax on any income in the biome. Decimals 3. /// The final value of tax is in the range [1..10]%, it depends on total liquidity staked by the guild uint internal constant BIOME_TAX_PERCENT_MIN = 1_000; // 1% /// @notice Max possible value of biome owner tax percent, decimals 3. uint internal constant BIOME_TAX_PERCENT_MAX = 10_000; // 10% /// @notice Make infinite approve of {token} to {spender} if the approved amount is less than {amount} /// @dev Should NOT be used for third-party pools function approveIfNeeded(address token, uint amount, address spender) internal { if (IERC20(token).allowance(address(this), spender) < amount) { IERC20(token).approve(spender, type(uint).max); } } /// @dev Remove from array the item with given id and move the last item on it place /// Use with mapping for keeping indexes in correct ordering function removeIndexed( uint256[] storage array, mapping(uint256 => uint256) storage indexes, uint256 id ) internal { uint256 lastId = array[array.length - 1]; uint256 index = indexes[id]; indexes[lastId] = index; indexes[id] = type(uint256).max; array[index] = lastId; array.pop(); } /// @notice Return a-b OR zero if a < b function sub0(uint32 a, uint32 b) internal pure returns (uint32) { return a > b ? a - b : 0; } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../solady/LibPRNG.sol"; library CalcLib { uint32 public constant MAX_CHANCE = 1e9; function minI32(int32 a, int32 b) internal pure returns (int32) { return a < b ? a : b; } function max32(int32 a, int32 b) internal pure returns (int32) { return a >= b ? a : b; } function absDiff(int32 a, int32 b) internal pure returns (uint32) { if (!((a >= 0 && b >= 0) || (a <= 0 && b <= 0))) revert IAppErrors.AbsDiff(a, b); if (a < 0) { a = - a; } if (b < 0) { b = - b; } return uint32(uint(int(a >= b ? a - b : b - a))); } function toUint(int32 n) internal pure returns (uint) { if (n <= 0) { return 0; } return uint(int(n)); } function toInt32(uint a) internal pure returns (int32){ if (a >= uint(int(type(int32).max))) { return type(int32).max; } return int32(int(a)); } /// @dev Simplified pseudo-random for minor functionality function pseudoRandom(uint maxValue) internal view returns (uint) { if (maxValue == 0) { return 0; } uint salt = genSalt(); // pseudo random number return (uint(keccak256(abi.encodePacked(blockhash(block.number), block.coinbase, block.difficulty, block.number, block.timestamp, tx.gasprice, gasleft(), salt))) % (maxValue + 1)); } function genSalt() internal view returns (uint salt) { // skale has a RNG Endpoint if ( block.chainid == uint(1351057110) || block.chainid == uint(37084624) ) { assembly { let freemem := mload(0x40) let start_addr := add(freemem, 0) if iszero(staticcall(gas(), 0x18, 0, 0, start_addr, 32)) { invalid() } salt := mload(freemem) } } } function pseudoRandomUint32(uint32 maxValue) internal view returns (uint32) { return uint32(pseudoRandom(uint(maxValue))); } /// @notice Generate pseudo-random uint in the range [0..maxValue) using Solady pseudo-random function function nextPrng(LibPRNG.PRNG memory prng, uint maxValue) internal pure returns (uint) { return LibPRNG.next(prng) % maxValue; } /// @notice pseudoRandomUint32 with customizable pseudoRandom() function pseudoRandomUint32Flex( uint32 maxValue, function (uint) internal view returns (uint) random_ ) internal view returns (uint32) { return uint32(random_(uint(maxValue))); } function pseudoRandomInt32(int32 maxValue) internal view returns (int32) { bool neg; if (maxValue < 0) { neg = true; maxValue = - maxValue; } uint32 v = uint32(pseudoRandom(uint(int(maxValue)))); return neg ? - int32(int(uint(v))) : int32(int(uint(v))); } /// @dev Simplified pseudo-random for minor functionality function pseudoRandomWithSeed(uint maxValue, uint seed) internal view returns (uint) { if (maxValue == 0) { return 0; } uint salt = genSalt(); // pseudo random number return (uint(keccak256(abi.encodePacked(blockhash(block.number), block.coinbase, block.difficulty, block.number, block.timestamp, tx.gasprice, gasleft(), seed, salt))) % (maxValue + 1)); } /// @dev Simplified pseudo-random for minor functionality, in range function pseudoRandomInRange(uint min, uint max) internal view returns (uint) { if (min >= max) { return max; } uint r = pseudoRandom(max - min); return min + r; } /// @dev Simplified pseudo-random for minor functionality, in range /// Equal to pseudoRandomInRange(min, max, pseudoRandom) function pseudoRandomInRangeFlex( uint min, uint max, function (uint) internal view returns (uint) random_ ) internal view returns (uint) { return min >= max ? max : min + random_(max - min); } function minusWithZeroFloor(uint a, uint b) internal pure returns (uint){ if (a <= b) { return 0; } return a - b; } function minusWithMinFloorI32(int32 a, int32 b) internal pure returns (int32){ if (int(a) - int(b) < type(int32).min) { return type(int32).min; } return a - b; } function plusWithMaxFloor32(int32 a, int32 b) internal pure returns (int32){ if (int(a) + int(b) >= type(int32).max) { return type(int32).max; } return a + b; } function sqrt(uint x) internal pure returns (uint z) { assembly { // Start off with z at 1. z := 1 // Used below to help find a nearby power of 2. let y := x // Find the lowest power of 2 that is at least sqrt(x). if iszero(lt(y, 0x100000000000000000000000000000000)) { y := shr(128, y) // Like dividing by 2 ** 128. z := shl(64, z) // Like multiplying by 2 ** 64. } if iszero(lt(y, 0x10000000000000000)) { y := shr(64, y) // Like dividing by 2 ** 64. z := shl(32, z) // Like multiplying by 2 ** 32. } if iszero(lt(y, 0x100000000)) { y := shr(32, y) // Like dividing by 2 ** 32. z := shl(16, z) // Like multiplying by 2 ** 16. } if iszero(lt(y, 0x10000)) { y := shr(16, y) // Like dividing by 2 ** 16. z := shl(8, z) // Like multiplying by 2 ** 8. } if iszero(lt(y, 0x100)) { y := shr(8, y) // Like dividing by 2 ** 8. z := shl(4, z) // Like multiplying by 2 ** 4. } if iszero(lt(y, 0x10)) { y := shr(4, y) // Like dividing by 2 ** 4. z := shl(2, z) // Like multiplying by 2 ** 2. } if iszero(lt(y, 0x8)) { // Equivalent to 2 ** z. z := shl(1, z) } // Shifting right by 1 is like dividing by 2. z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) // Compute a rounded down version of z. let zRoundDown := div(x, z) // If zRoundDown is smaller, use it. if lt(zRoundDown, z) { z := zRoundDown } } } /********************************************* * PRB-MATH * * https://github.com/hifi-finance/prb-math * **********************************************/ /// @notice Calculates the binary logarithm of x. /// /// @dev Based on the iterative approximation algorithm. /// https://en.wikipedia.org/wiki/Binary_logarithm#Iterative_approximation /// /// Requirements: /// - x must be greater than or equal to SCALE, otherwise the result would be negative. /// /// Caveats: /// - The results are nor perfectly accurate to the last decimal, /// due to the lossy precision of the iterative approximation. /// /// @param x The unsigned 60.18-decimal fixed-point number for which /// to calculate the binary logarithm. /// @return result The binary logarithm as an unsigned 60.18-decimal fixed-point number. function log2(uint256 x) internal pure returns (uint256 result) { if (x < 1e18) revert IAppErrors.TooLowX(x); // Calculate the integer part of the logarithm // and add it to the result and finally calculate y = x * 2^(-n). uint256 n = mostSignificantBit(x / 1e18); // The integer part of the logarithm as an unsigned 60.18-decimal fixed-point number. // The operation can't overflow because n is maximum 255 and SCALE is 1e18. uint256 rValue = n * 1e18; // This is y = x * 2^(-n). uint256 y = x >> n; // If y = 1, the fractional part is zero. if (y == 1e18) { return rValue; } // Calculate the fractional part via the iterative approximation. // The "delta >>= 1" part is equivalent to "delta /= 2", but shifting bits is faster. for (uint256 delta = 5e17; delta > 0; delta >>= 1) { y = (y * y) / 1e18; // Is y^2 > 2 and so in the range [2,4)? if (y >= 2 * 1e18) { // Add the 2^(-m) factor to the logarithm. rValue += delta; // Corresponds to z/2 on Wikipedia. y >>= 1; } } return rValue; } /// @notice Finds the zero-based index of the first one in the binary representation of x. /// @dev See the note on msb in the "Find First Set" /// Wikipedia article https://en.wikipedia.org/wiki/Find_first_set /// @param x The uint256 number for which to find the index of the most significant bit. /// @return msb The index of the most significant bit as an uint256. //noinspection NoReturn function mostSignificantBit(uint256 x) internal pure returns (uint256 msb) { if (x >= 2 ** 128) { x >>= 128; msb += 128; } if (x >= 2 ** 64) { x >>= 64; msb += 64; } if (x >= 2 ** 32) { x >>= 32; msb += 32; } if (x >= 2 ** 16) { x >>= 16; msb += 16; } if (x >= 2 ** 8) { x >>= 8; msb += 8; } if (x >= 2 ** 4) { x >>= 4; msb += 4; } if (x >= 2 ** 2) { x >>= 2; msb += 2; } if (x >= 2 ** 1) { // No need to shift x any more. msb += 1; } } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IController.sol"; import "../interfaces/IOracle.sol"; import "../interfaces/IStatController.sol"; import "../interfaces/IStoryController.sol"; import "../interfaces/ITreasury.sol"; import "../interfaces/IDungeonFactory.sol"; import "../interfaces/IReinforcementController.sol"; import "../interfaces/IGameToken.sol"; import "../interfaces/IGOC.sol"; import "../interfaces/IItemController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IUserController.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IRewardsPool.sol"; import "../interfaces/IPvpController.sol"; import "../interfaces/IItemBoxController.sol"; /// @notice Provide context-struct with all controller addresses and routines for lazy init /// Usage: /// Create an instance of the structure /// cc = ControllerContextLib.init(controller); /// access controller directly /// cc.controller.xxx(); /// access other contracts indirectly /// sc = ControllerContextLib.statController(cc); library ControllerContextLib { //region ----------------- Data types enum CacheIndex { STAT_CONTROLLER_0, STORY_CONTROLLER_1, ORACLE_2, TREASURY_3, DUNGEON_FACTORY_4, GOC_5, REINFORCEMENT_CONTROLLER_6, ITEM_CONTROLLER_7, HERO_CONTROLLER_8, GAME_TOKEN_9, USER_CONTROLLER_10, GUILD_CONTROLLER_11, PVP_CONTROLLER_12, REWARDS_POOL_13, ITEM_BOX_CONTROLLER_14 } uint constant private CACHE_SIZE = 15; struct ControllerContext { /// @notice Direct access to the controller IController controller; /// @notice All lazy-initialized addresses in order of {CacheIndex} address[CACHE_SIZE] cache; } //endregion ----------------- Data types //region ----------------- Initialization and _lazyInit function init(IController controller) internal pure returns (ControllerContext memory cc) { cc.controller = controller; return cc; } function _lazyInit( ControllerContext memory cc, CacheIndex index, function () external view returns(address) getter ) internal view returns (address) { address a = cc.cache[uint(index)]; if (a != address(0)) return a; cc.cache[uint(index)] = getter(); return cc.cache[uint(index)]; } //endregion ----------------- Initialization and _lazyInit //region ----------------- Access with lazy initialization function statController(ControllerContext memory cc) internal view returns (IStatController) { return IStatController(_lazyInit(cc, CacheIndex.STAT_CONTROLLER_0, cc.controller.statController)); } function storyController(ControllerContext memory cc) internal view returns (IStoryController) { return IStoryController(_lazyInit(cc, CacheIndex.STORY_CONTROLLER_1, cc.controller.storyController)); } function oracle(ControllerContext memory cc) internal view returns (IOracle) { return IOracle(_lazyInit(cc, CacheIndex.ORACLE_2, cc.controller.oracle)); } function treasury(ControllerContext memory cc) internal view returns (ITreasury) { return ITreasury(_lazyInit(cc, CacheIndex.TREASURY_3, cc.controller.treasury)); } function dungeonFactory(ControllerContext memory cc) internal view returns (IDungeonFactory) { return IDungeonFactory(_lazyInit(cc, CacheIndex.DUNGEON_FACTORY_4, cc.controller.dungeonFactory)); } function gameObjectController(ControllerContext memory cc) internal view returns (IGOC) { return IGOC(_lazyInit(cc, CacheIndex.GOC_5, cc.controller.gameObjectController)); } function reinforcementController(ControllerContext memory cc) internal view returns (IReinforcementController) { return IReinforcementController(_lazyInit(cc, CacheIndex.REINFORCEMENT_CONTROLLER_6, cc.controller.reinforcementController)); } function itemController(ControllerContext memory cc) internal view returns (IItemController) { return IItemController(_lazyInit(cc, CacheIndex.ITEM_CONTROLLER_7, cc.controller.itemController)); } function heroController(ControllerContext memory cc) internal view returns (IHeroController) { return IHeroController(_lazyInit(cc, CacheIndex.HERO_CONTROLLER_8, cc.controller.heroController)); } function gameToken(ControllerContext memory cc) internal view returns (IGameToken) { return IGameToken(_lazyInit(cc, CacheIndex.GAME_TOKEN_9, cc.controller.gameToken)); } function userController(ControllerContext memory cc) internal view returns (IUserController) { return IUserController(_lazyInit(cc, CacheIndex.USER_CONTROLLER_10, cc.controller.userController)); } function guildController(ControllerContext memory cc) internal view returns (IGuildController) { return IGuildController(_lazyInit(cc, CacheIndex.GUILD_CONTROLLER_11, cc.controller.guildController)); } function pvpController(ControllerContext memory cc) internal view returns (IPvpController) { return IPvpController(_lazyInit(cc, CacheIndex.PVP_CONTROLLER_12, cc.controller.pvpController)); } function rewardsPool(ControllerContext memory cc) internal view returns (IRewardsPool) { return IRewardsPool(_lazyInit(cc, CacheIndex.REWARDS_POOL_13, cc.controller.rewardsPool)); } function itemBoxController(ControllerContext memory cc) internal view returns (IItemBoxController) { return IItemBoxController(_lazyInit(cc, CacheIndex.ITEM_BOX_CONTROLLER_14, cc.controller.itemBoxController)); } //endregion ----------------- Access with lazy initialization }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IStatController.sol"; import "../interfaces/IItemController.sol"; import "../interfaces/IFightCalculator.sol"; import "../interfaces/IAppErrors.sol"; import "../interfaces/IApplicationEvents.sol"; import "../lib/StatLib.sol"; import "../lib/CalcLib.sol"; import "../lib/PackingLib.sol"; import "../solady/FixedPointMathLib.sol"; library FightLib { using PackingLib for bytes32; using CalcLib for int32; //region ------------------------ Data types struct AttackResult { int32 defenderHealth; int32 damage; int32 lifeStolen; int32 reflectDamage; uint8 critical; uint8 missed; uint8 blocked; } //endregion ------------------------ Data types //region ------------------------ Constants uint internal constant MAX_FIGHT_CYCLES = 100; int32 internal constant RESISTANCE_DENOMINATOR = 100; int32 internal constant _MAX_RESIST = 90; /// @notice SIP-002 constant: desired capacity uint internal constant CAPACITY_RESISTS_DEFS = 90; /// @notice SIP-002 constant: desired capacity uint internal constant CAPACITY_CRITICAL_HIT_STATUSES = 100; /// @notice SIP-002 constant: the factor of how fast the value will reach the capacity uint internal constant K_FACTOR = 100; /// @notice ln(2), decimals 18 int internal constant LN2 = 693147180559945309; //endregion ------------------------ Constants //region ------------------------ Main logic /// @dev Items ownership must be checked before /// it is no write actions but we need to emit an event for properly handle the battle on UI /// return huge structs more expensive that call an event here /// @param random_ Pass _pseudoRandom here, param is required for unit tests function fight( IItemController ic, IFightCalculator.FightCall memory callData, IFightCalculator.FightCallAdd memory callDataAdd, function (uint) internal view returns (uint) random_ ) internal returns ( IFightCalculator.FightResult memory ) { IFightCalculator.FightInfoInternal memory fResult = prepareFightInternalInfo(ic, callData.fighterA, callData.fighterB); fightProcessing(fResult, random_); if (callDataAdd.fightId == 0) { // not pvp fight emit IApplicationEvents.FightResultProcessed(callDataAdd.msgSender, fResult, callData, callData.iteration); } else { // pvp fight emit IApplicationEvents.PvpFightResultProcessed(callDataAdd.fightId, callDataAdd.msgSender, fResult, callData.turn, callData.heroAdr, callData.heroId); } return IFightCalculator.FightResult({ healthA: fResult.fighterA.health, healthB: fResult.fighterB.health, manaConsumedA: fResult.fighterA.manaConsumed, manaConsumedB: fResult.fighterB.manaConsumed }); } //endregion ------------------------ Main logic //region ------------------------ High level of internal logic function fightProcessing( IFightCalculator.FightInfoInternal memory fResult, function (uint) internal view returns (uint) random_ ) internal view { bool firstA = calcFirstHit(fResult); setStatuses(fResult, firstA, random_); setStatuses(fResult, !firstA, random_); reduceAttributesByStatuses(fResult.fighterA.info.fighterAttributes, fResult.fighterA.statuses, fResult.fighterB.info.fighterAttributes); reduceAttributesByStatuses(fResult.fighterB.info.fighterAttributes, fResult.fighterB.statuses, fResult.fighterA.info.fighterAttributes); AttackResult memory resultA = processAttack(fResult, true, random_); AttackResult memory resultB = processAttack(fResult, false, random_); fResult.fighterA.statuses.gotCriticalHit = resultA.critical != 0; fResult.fighterA.statuses.missed = resultA.missed != 0; fResult.fighterA.statuses.hitBlocked = resultA.blocked != 0; fResult.fighterB.statuses.gotCriticalHit = resultB.critical != 0; fResult.fighterB.statuses.missed = resultB.missed != 0; fResult.fighterB.statuses.hitBlocked = resultB.blocked != 0; reduceHp( firstA ? resultA : resultB, firstA ? resultB : resultA, firstA ? fResult.fighterA : fResult.fighterB, firstA ? fResult.fighterB : fResult.fighterA ); // restore health from stolen life stealLife(fResult.fighterA, resultA); stealLife(fResult.fighterB, resultB); } function processAttack( IFightCalculator.FightInfoInternal memory fResult, bool isA, function (uint) internal view returns (uint) random_ ) internal view returns (AttackResult memory attackResult) { int32 defenderHealth = isA ? fResult.fighterB.health : fResult.fighterA.health; if (skipTurn(fResult, isA)) { return AttackResult({ defenderHealth: defenderHealth, damage: 0, lifeStolen: 0, reflectDamage: 0, critical: 0, missed: 0, blocked: 0 }); } IFightCalculator.FighterInfo memory attackerInfo = isA ? fResult.fighterA.info : fResult.fighterB.info; IFightCalculator.FighterInfo memory defenderInfo = isA ? fResult.fighterB.info : fResult.fighterA.info; if (attackerInfo.attackType == IFightCalculator.AttackType.MELEE) { attackResult = meleeDamageCalculation(attackerInfo, defenderInfo, defenderHealth, random_); } else if (attackerInfo.attackType == IFightCalculator.AttackType.MAGIC) { attackResult = magicDamageCalculation( attackerInfo, defenderInfo, isA ? fResult.fighterA.magicAttack : fResult.fighterB.magicAttack, defenderHealth, random_ ); } else { revert IAppErrors.NotAType(uint(attackerInfo.attackType)); } } //endregion ------------------------ High level of internal logic //region ------------------------ Internal logic function prepareFightInternalInfo( IItemController ic, IFightCalculator.FighterInfo memory fighterA, IFightCalculator.FighterInfo memory fighterB ) internal view returns (IFightCalculator.FightInfoInternal memory) { IFightCalculator.FightInfoInternal memory fInfo; _setFightData(ic, fighterA, fInfo.fighterA); _setFightData(ic, fighterB, fInfo.fighterB); return fInfo; } /// @dev A part of prepareFightInternalInfo function _setFightData( IItemController ic, IFightCalculator.FighterInfo memory fighter, IFightCalculator.Fighter memory dest ) internal view { dest.info = fighter; dest.health = int32(fighter.fighterStats.life); if (fighter.attackToken != address(0)) { if (fighter.attackType != IFightCalculator.AttackType.MAGIC) revert IAppErrors.NotMagic(); dest.magicAttack = ic.itemAttackInfo(fighter.attackToken, fighter.attackTokenId); } // dest.manaConsumed is 0 by default, in current implementation we don't need to change it } /// @param random_ Either _pseudoRandom or pseudo-random for ut function statusChance( IFightCalculator.FighterInfo memory attackerInfo, IItemController.AttackInfo memory attackerMA, IStatController.ATTRIBUTES index, int32 resist, function (uint) internal view returns (uint) random_ ) internal view returns (bool) { int32 chance = _getChance(attackerInfo, attackerMA.aType, index, resist); if (chance == 0) { return false; } if (chance >= RESISTANCE_DENOMINATOR) { return true; } return random_(RESISTANCE_DENOMINATOR.toUint()) < chance.toUint(); } /// @notice set fResult.fighterB.statuses (for isA = true) or fResult.fighterA.statuses (for isA = false) /// @param random_ Either _pseudoRandom or pseudo-random for ut function setStatuses( IFightCalculator.FightInfoInternal memory fResult, bool isA, function (uint) internal view returns (uint) random_ ) internal view { // setStatuses is called twice one by one: first time for A, second time for B // if stun is set for A, setStatuses is skipped for B completely if (!skipTurn(fResult, isA)) { IFightCalculator.FighterInfo memory attackerInfo = isA ? fResult.fighterA.info : fResult.fighterB.info; IFightCalculator.FighterInfo memory defenderInfo = isA ? fResult.fighterB.info : fResult.fighterA.info; IItemController.AttackInfo memory attackerMA = isA ? fResult.fighterA.magicAttack : fResult.fighterB.magicAttack; IFightCalculator.Statuses memory statuses = isA ? fResult.fighterB.statuses : fResult.fighterA.statuses; int32 resist = _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.RESIST_TO_STATUSES); statuses.stun = statusChance(attackerInfo, attackerMA, IStatController.ATTRIBUTES.STUN, resist, random_); statuses.burn = statusChance( attackerInfo, attackerMA, IStatController.ATTRIBUTES.BURN, _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.FIRE_RESISTANCE), random_ ); statuses.freeze = statusChance( attackerInfo, attackerMA, IStatController.ATTRIBUTES.FREEZE, _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.COLD_RESISTANCE), random_ ); statuses.confuse = statusChance(attackerInfo, attackerMA, IStatController.ATTRIBUTES.CONFUSE, resist, random_); statuses.curse = statusChance(attackerInfo, attackerMA, IStatController.ATTRIBUTES.CURSE, resist, random_); statuses.poison = statusChance(attackerInfo, attackerMA, IStatController.ATTRIBUTES.POISON, resist, random_); } } function magicDamageCalculation( IFightCalculator.FighterInfo memory attackerInfo, IFightCalculator.FighterInfo memory defenderInfo, IItemController.AttackInfo memory magicAttack, int32 defenderHealth, function (uint) internal view returns (uint) random_ ) internal view returns (AttackResult memory attackResult) { // generate damage int32 damage = getMagicDamage( attackerInfo, magicAttack, CalcLib.pseudoRandomInRangeFlex(magicAttack.min.toUint(), magicAttack.max.toUint(), random_) ); damage = increaseMagicDmgByFactor(damage, attackerInfo, magicAttack.aType); damage = increaseRaceDmg(damage, attackerInfo, defenderInfo.race); bool critical = isCriticalHit(attackerInfo, random_(RESISTANCE_DENOMINATOR.toUint())); damage = critical ? damage * 2 : damage; // decrease damage damage = decreaseRaceDmg(damage, defenderInfo, attackerInfo.race); damage = decreaseDmgByDmgReduction(damage, defenderInfo); if (magicAttack.aType == IItemController.AttackType.FIRE) { damage -= _calcDmgInline(damage, defenderInfo, IStatController.ATTRIBUTES.FIRE_RESISTANCE); } else if (magicAttack.aType == IItemController.AttackType.COLD) { damage -= _calcDmgInline(damage, defenderInfo, IStatController.ATTRIBUTES.COLD_RESISTANCE); } else if (magicAttack.aType == IItemController.AttackType.LIGHTNING) { damage -= _calcDmgInline(damage, defenderInfo, IStatController.ATTRIBUTES.LIGHTNING_RESISTANCE); } int32 defenderHealthResult = defenderHealth < damage ? int32(0) : defenderHealth - damage; damage = defenderHealth - defenderHealthResult; return AttackResult({ defenderHealth: defenderHealthResult, damage: damage, lifeStolen: lifeStolenPerHit(damage, attackerInfo), reflectDamage: reflectMagicDmg(damage, defenderInfo) + reflectChaos(magicAttack, attackerInfo, random_(1e18)), critical: critical ? uint8(1) : uint8(0), missed: 0, blocked: 0 }); } function meleeDamageCalculation( IFightCalculator.FighterInfo memory attackerInfo, IFightCalculator.FighterInfo memory defenderInfo, int32 defenderHealth, function (uint) internal view returns (uint) random_ ) internal view returns (AttackResult memory attackResult) { attackResult = (new AttackResult[](1))[0]; // generate damage int32 damage = getDamage(attackerInfo.fighterAttributes, random_); damage = increaseMeleeDmgByFactor(damage, attackerInfo); damage = increaseRaceDmg(damage, attackerInfo, defenderInfo.race); attackResult.critical = isCriticalHit(attackerInfo, random_(RESISTANCE_DENOMINATOR.toUint())) ? uint8(1) : uint8(0); damage = attackResult.critical == 0 ? damage : damage * 2; // decrease damage damage = decreaseRaceDmg(damage, defenderInfo, attackerInfo.race); damage = decreaseDmgByDmgReduction(damage, defenderInfo); attackResult.missed = random_(1e18) > StatLib.chanceToHit( _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.ATTACK_RATING).toUint(), _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.DEFENSE).toUint(), attackerInfo.fighterStats.level, defenderInfo.fighterStats.level, _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.AR_FACTOR).toUint() ) ? 1 : 0; attackResult.blocked = (random_(100) < _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.BLOCK_RATING).toUint()) ? 1 : 0; if (attackResult.missed != 0 || attackResult.blocked != 0) { damage = 0; } int32 defenderHealthResult = defenderHealth <= damage ? int32(0) : defenderHealth - damage; damage = defenderHealth - defenderHealthResult; attackResult.defenderHealth = defenderHealthResult; attackResult.damage = damage; attackResult.lifeStolen = lifeStolenPerHit(damage, attackerInfo); attackResult.reflectDamage = reflectMeleeDmg(damage, defenderInfo); } function getDamage( int32[] memory attributes, function (uint) internal view returns (uint) random_ ) internal view returns (int32) { return int32(int(CalcLib.pseudoRandomInRangeFlex( _getAttrValue(attributes, IStatController.ATTRIBUTES.DAMAGE_MIN).toUint(), _getAttrValue(attributes, IStatController.ATTRIBUTES.DAMAGE_MAX).toUint(), random_ ))); } //endregion ------------------------ Internal logic //region ------------------------ Pure utils /// @notice Modify values in {targetAttributes} and {casterAttributes} according to {statuses} function reduceAttributesByStatuses( int32[] memory targetAttributes, IFightCalculator.Statuses memory statuses, int32[] memory casterAttributes ) internal pure { if (statuses.burn) { targetAttributes[uint(IStatController.ATTRIBUTES.DEFENSE)] -= (targetAttributes[uint(IStatController.ATTRIBUTES.DEFENSE)] / 3); targetAttributes[uint(IStatController.ATTRIBUTES.COLD_RESISTANCE)] += 50; casterAttributes[uint(IStatController.ATTRIBUTES.CRITICAL_HIT)] += 10; casterAttributes[uint(IStatController.ATTRIBUTES.DESTROY_ITEMS)] += 20; } if (statuses.freeze) { targetAttributes[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)] /= 2; targetAttributes[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)] /= 2; targetAttributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] -= targetAttributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] / 3; targetAttributes[uint(IStatController.ATTRIBUTES.BLOCK_RATING)] /= 2; targetAttributes[uint(IStatController.ATTRIBUTES.FIRE_RESISTANCE)] += 50; } if (statuses.confuse) { targetAttributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] /= 2; } if (statuses.curse) { targetAttributes[uint(IStatController.ATTRIBUTES.FIRE_RESISTANCE)] /= 2; targetAttributes[uint(IStatController.ATTRIBUTES.COLD_RESISTANCE)] /= 2; targetAttributes[uint(IStatController.ATTRIBUTES.LIGHTNING_RESISTANCE)] /= 2; } if (statuses.stun) { casterAttributes[uint(IStatController.ATTRIBUTES.CRITICAL_HIT)] += 10; } if (statuses.poison) { targetAttributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] /= 2; } } /// @notice Calculate new damage value depending on {defenderRace} and value of corresponded DMG_AGAINST_XXX attribute /// @param defenderRace See IStatController.Race /// @return Updated damage value function increaseRaceDmg(int32 dmg, IFightCalculator.FighterInfo memory attackerInfo, uint defenderRace) internal pure returns (int32) { if (defenderRace == uint(IStatController.Race.HUMAN)) { return dmg + _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.DMG_AGAINST_HUMAN) * dmg / RESISTANCE_DENOMINATOR; } else if (defenderRace == uint(IStatController.Race.UNDEAD)) { return dmg + _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.DMG_AGAINST_UNDEAD) * dmg / RESISTANCE_DENOMINATOR; } else if (defenderRace == uint(IStatController.Race.DAEMON)) { return dmg + _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.DMG_AGAINST_DAEMON) * dmg / RESISTANCE_DENOMINATOR; } else if (defenderRace == uint(IStatController.Race.BEAST)) { return dmg + _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.DMG_AGAINST_BEAST) * dmg / RESISTANCE_DENOMINATOR; } else { return dmg; } } /// @notice Decrease damage depending on {attackerRace} function decreaseRaceDmg(int32 dmg, IFightCalculator.FighterInfo memory defenderInfo, uint attackerRace) internal pure returns (int32) { if (attackerRace == uint(IStatController.Race.HUMAN)) { return dmg - _calcDmgInline(dmg, defenderInfo, IStatController.ATTRIBUTES.DEF_AGAINST_HUMAN); } else if (attackerRace == uint(IStatController.Race.UNDEAD)) { return dmg - _calcDmgInline(dmg, defenderInfo, IStatController.ATTRIBUTES.DEF_AGAINST_UNDEAD); } else if (attackerRace == uint(IStatController.Race.DAEMON)) { return dmg - _calcDmgInline(dmg, defenderInfo, IStatController.ATTRIBUTES.DEF_AGAINST_DAEMON); } else if (attackerRace == uint(IStatController.Race.BEAST)) { return dmg - _calcDmgInline(dmg, defenderInfo, IStatController.ATTRIBUTES.DEF_AGAINST_BEAST); } else { return dmg; } } /// @notice Calculate damage after Melee-attack function increaseMeleeDmgByFactor(int32 dmg, IFightCalculator.FighterInfo memory attackerInfo) internal pure returns (int32){ return dmg + _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.MELEE_DMG_FACTOR) * dmg / RESISTANCE_DENOMINATOR; } /// @notice Calculate damage after Magic-attack function increaseMagicDmgByFactor(int32 dmg, IFightCalculator.FighterInfo memory attackerInfo, IItemController.AttackType aType) internal pure returns (int32) { if (aType == IItemController.AttackType.FIRE) { return dmg + dmg * _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.FIRE_DMG_FACTOR) / RESISTANCE_DENOMINATOR; } else if (aType == IItemController.AttackType.COLD) { return dmg + dmg * _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.COLD_DMG_FACTOR) / RESISTANCE_DENOMINATOR; } else if (aType == IItemController.AttackType.LIGHTNING) { return dmg + dmg * _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.LIGHTNING_DMG_FACTOR) / RESISTANCE_DENOMINATOR; } else { return dmg; } } /// @notice Reduce damage depending on value of Damage Reduction attribute function decreaseDmgByDmgReduction(int32 dmg, IFightCalculator.FighterInfo memory defenderInfo) internal pure returns (int32) { return dmg - _calcDmgInline(dmg, defenderInfo, IStatController.ATTRIBUTES.DAMAGE_REDUCTION); } /// @notice Calculate poison damage < {health} function poisonDmg(int32 health, IFightCalculator.Statuses memory statuses) internal pure returns (int32) { // poison should not kill if (statuses.poison && health.toUint() > 1) { // at least 1 dmg return int32(int(Math.max(health.toUint() / 10, 1))); } return 0; } /// @notice Reduce health of the fighters according to attacks results, calc damagePoison, damage and damageReflect. function reduceHp( AttackResult memory firstAttack, AttackResult memory secondAttack, IFightCalculator.Fighter memory firstFighter, IFightCalculator.Fighter memory secondFighter ) internal pure { secondFighter.health = firstAttack.defenderHealth; firstFighter.damage = firstAttack.damage; // hit only if second fighter survived if (secondFighter.health != 0) { firstFighter.health = secondAttack.defenderHealth; secondFighter.damage = secondAttack.damage; // reflect damage from second to first secondFighter.damageReflect = (CalcLib.minI32(firstAttack.reflectDamage, firstFighter.health)); firstFighter.health -= secondFighter.damageReflect; // reflect damage from first to second firstFighter.damageReflect = (CalcLib.minI32(secondAttack.reflectDamage, secondFighter.health)); secondFighter.health -= firstFighter.damageReflect; } // poison second firstly (he got damage and statuses early) firstFighter.damagePoison = poisonDmg(secondFighter.health, secondFighter.statuses); secondFighter.health -= firstFighter.damagePoison; // poison first fighter secondFighter.damagePoison = poisonDmg(firstFighter.health, firstFighter.statuses); firstFighter.health -= secondFighter.damagePoison; } /// @notice Calculate life-stolen-per-hit value for the given {damage} value function lifeStolenPerHit(int32 dmg, IFightCalculator.FighterInfo memory attackerInfo) internal pure returns (int32) { return dmg * _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.LIFE_STOLEN_PER_HIT) / RESISTANCE_DENOMINATOR; } /// @notice Increase {fighter.health} on the value of life-stolen-per-hit (only if the health > 0) function stealLife(IFightCalculator.Fighter memory fighter, AttackResult memory attackResult) internal pure { if (fighter.health != 0) { int32 newHealth = fighter.health + attackResult.lifeStolen; int32 maxHealth = _getAttrValue(fighter.info.fighterAttributes, IStatController.ATTRIBUTES.LIFE); fighter.health = (CalcLib.minI32(newHealth, maxHealth)); } } function skipTurn(IFightCalculator.FightInfoInternal memory fResult, bool isA) internal pure returns (bool) { return isA ? fResult.fighterA.statuses.stun : fResult.fighterB.statuses.stun; } /// @notice Detect which hero is faster and makes the hit first. Magic is faster melee. /// Otherwise first hit is made by the fighter with higher attack rating (A is selected if the ratings are equal) function calcFirstHit(IFightCalculator.FightInfoInternal memory fInfo) internal pure returns (bool aFirst){ if (fInfo.fighterA.info.attackType == IFightCalculator.AttackType.MAGIC) { if (fInfo.fighterB.info.attackType == IFightCalculator.AttackType.MAGIC) { // if both fighters use magic we check attack rating aFirst = isAttackerFaster(fInfo.fighterA.info, fInfo.fighterB.info); } else { // otherwise, magic always faster than melee aFirst = true; } } else { if (fInfo.fighterB.info.attackType == IFightCalculator.AttackType.MAGIC) { // if fighter use magic he will be faster aFirst = false; } else { // otherwise, check attack rating aFirst = isAttackerFaster(fInfo.fighterA.info, fInfo.fighterB.info); } } } function isAttackerFaster( IFightCalculator.FighterInfo memory fighterAInfo, IFightCalculator.FighterInfo memory fighterBInfo ) internal pure returns (bool) { return _getAttrValue(fighterAInfo.fighterAttributes, IStatController.ATTRIBUTES.ATTACK_RATING) >= _getAttrValue(fighterBInfo.fighterAttributes, IStatController.ATTRIBUTES.ATTACK_RATING); } function reflectMeleeDmg(int32 dmg, IFightCalculator.FighterInfo memory defenderInfo) internal pure returns (int32) { return dmg * _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.REFLECT_DAMAGE_MELEE) / RESISTANCE_DENOMINATOR; } function reflectMagicDmg(int32 dmg, IFightCalculator.FighterInfo memory defenderInfo) internal pure returns (int32) { return dmg * _getAttrValue(defenderInfo.fighterAttributes, IStatController.ATTRIBUTES.REFLECT_DAMAGE_MAGIC) / RESISTANCE_DENOMINATOR; } function _getChance( IFightCalculator.FighterInfo memory attackerInfo, IItemController.AttackType aType, IStatController.ATTRIBUTES index, int32 resist ) internal pure returns (int32 chance) { int32 chanceBase = attackerInfo.fighterAttributes[uint(index)]; if (attackerInfo.attackType == IFightCalculator.AttackType.MAGIC) { if (index == IStatController.ATTRIBUTES.BURN && aType == IItemController.AttackType.FIRE) { chanceBase += int32(20); } if (index == IStatController.ATTRIBUTES.FREEZE && aType == IItemController.AttackType.COLD) { chanceBase += int32(20); } if (index == IStatController.ATTRIBUTES.CONFUSE && aType == IItemController.AttackType.LIGHTNING) { chanceBase += int32(20); } } chance = _getAdjustedAttributeValue(chanceBase, index); return chance - chance * (CalcLib.minI32(resist, _MAX_RESIST)) / RESISTANCE_DENOMINATOR; } /// @param randomValue Result of call _pseudoRandom, value in the range [0...RESISTANCE_DENOMINATOR) function isCriticalHit( IFightCalculator.FighterInfo memory attackerInfo, uint randomValue ) internal pure returns (bool) { return randomValue < _getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.CRITICAL_HIT).toUint(); } /// @param randomValue Result of call CalcLib.pseudoRandom(1e18) function reflectChaos( IItemController.AttackInfo memory magicAttack, IFightCalculator.FighterInfo memory attackerInfo, uint randomValue ) internal pure returns (int32) { return (magicAttack.aType == IItemController.AttackType.CHAOS && randomValue > 5e17) ? int32(attackerInfo.fighterStats.life) / int32(2) : int32(0); } function _calcDmgInline(int32 dmg, IFightCalculator.FighterInfo memory info, IStatController.ATTRIBUTES index) internal pure returns (int32) { return dmg * (CalcLib.minI32(_getAttrValue(info.fighterAttributes, index), _MAX_RESIST)) / RESISTANCE_DENOMINATOR; } function getMagicDamage( IFightCalculator.FighterInfo memory attackerInfo, IItemController.AttackInfo memory mAttack, uint randomValue_ ) internal pure returns (int32) { int32 attributeFactorResult = (_getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.STRENGTH) * mAttack.attributeFactors.strength / 100); attributeFactorResult += (_getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.DEXTERITY) * mAttack.attributeFactors.dexterity / 100); attributeFactorResult += (_getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.VITALITY) * mAttack.attributeFactors.vitality / 100); attributeFactorResult += (_getAttrValue(attackerInfo.fighterAttributes, IStatController.ATTRIBUTES.ENERGY) * mAttack.attributeFactors.energy / 100); return int32(int(randomValue_)) + attributeFactorResult; } //endregion ------------------------ Pure utils //region ------------------------ SIP-002 /// @notice SIP-002: Implement smooth increase that approaches to y0 but never reaches that value /// @dev https://discord.com/channels/1134537718039318608/1265261881652674631 /// @param y0 is desired capacity, 90 for resists/defs, 100 for critical hit and statuses /// @param x current value, base attribute. Assume x >= 0 /// @param k is the factor of how fast the value will reach 90 capacity, k=100 by default /// @return new attribute value that is used in calculations, decimals 18 function getReducedValue(uint y0, uint x, uint k) internal pure returns (uint) { // 2^n = exp(ln(2^n)) = exp(n * ln2) int t = FixedPointMathLib.expWad(-int(x) * LN2 / int(k)); return t < 0 ? 0 // some mistake happens (???) : y0 * (1e18 - uint(t)); } /// @notice Apply {getReducedValue} to the given attribute, change value in place function _getAdjustedValue(int32 attributeValue, uint y0, uint k) internal pure returns (int32) { return attributeValue <= 0 ? int32(0) // negative values => 0 : int32(int(getReducedValue(y0, uint(int(attributeValue)), k) / 1e18)); } /// @notice Return adjusted attribute value. Adjust selected attributes using y=z(1−2^(−x/k)) formula /// Value in array {attributes} is NOT changed. function _getAttrValue(int32[] memory attributes, IStatController.ATTRIBUTES attrId) internal pure returns (int32) { return _getAdjustedAttributeValue(attributes[uint(attrId)], attrId); } function _getAdjustedAttributeValue(int32 value, IStatController.ATTRIBUTES attrId) internal pure returns (int32) { if ( attrId == IStatController.ATTRIBUTES.BLOCK_RATING || attrId == IStatController.ATTRIBUTES.FIRE_RESISTANCE || attrId == IStatController.ATTRIBUTES.COLD_RESISTANCE || attrId == IStatController.ATTRIBUTES.LIGHTNING_RESISTANCE || attrId == IStatController.ATTRIBUTES.DEF_AGAINST_HUMAN || attrId == IStatController.ATTRIBUTES.DEF_AGAINST_UNDEAD || attrId == IStatController.ATTRIBUTES.DEF_AGAINST_DAEMON || attrId == IStatController.ATTRIBUTES.DEF_AGAINST_BEAST || attrId == IStatController.ATTRIBUTES.DAMAGE_REDUCTION || attrId == IStatController.ATTRIBUTES.RESIST_TO_STATUSES ) { // use CAPACITY_RESISTS_DEFS, K_FACTOR return _getAdjustedValue(value, CAPACITY_RESISTS_DEFS, K_FACTOR); } else if ( attrId == IStatController.ATTRIBUTES.CRITICAL_HIT || attrId == IStatController.ATTRIBUTES.STUN || attrId == IStatController.ATTRIBUTES.BURN || attrId == IStatController.ATTRIBUTES.FREEZE || attrId == IStatController.ATTRIBUTES.CONFUSE || attrId == IStatController.ATTRIBUTES.CURSE || attrId == IStatController.ATTRIBUTES.POISON ) { // use CAPACITY_CRITICAL_HIT_STATUSES, K_FACTOR return _getAdjustedValue(value, CAPACITY_CRITICAL_HIT_STATUSES, K_FACTOR); } else { return value; } } //endregion ------------------------ SIP-002 }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IApplicationEvents.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IItem.sol"; import "../interfaces/IItemController.sol"; import "../interfaces/IOracle.sol"; import "../openzeppelin/Math.sol"; import "../solady/LibPRNG.sol"; import "./CalcLib.sol"; import "./ControllerContextLib.sol"; import "./ShelterLib.sol"; import "./StatLib.sol"; library ItemLib { using CalcLib for int32; using PackingLib for address; using PackingLib for bytes32; using PackingLib for bytes32[]; using PackingLib for uint32[]; using PackingLib for int32[]; //region ------------------------ Data types struct GenerateAttributesContext { /// @notice True if max allowed amount of random attributes were reached inside {_prepareAttributes} bool stopGenerateRandom; /// @notice Flag - attribute was generated. The array matches to info.ids bool[] usedIndexes; /// @notice Ids of the generated attributes uint8[] ids; /// @notice Randomly selected values of the generated attributes int32[] values; /// @notice Counter of the stored values into {ids} and {values} uint counter; /// @notice Total number of random attributes that were generated inside {_prepareAttributes} uint randomAttrCounter; /// @notice Total sum of all {random} values for random attributes generated in {_prepareAttributes} uint randomSum; /// @notice Total sum of all chances of the random attributes generated in {_prepareAttributes} uint chancesSum; /// @notice Total number of random attributes that can be generated uint totalRandomAttrsPossible; } struct MintItemInfo { uint8 maxItems; int32 magicFind; int32 destroyItems; uint32[] mintItemsChances; IOracle oracle; address[] mintItems; uint amplifier; uint seed; /// @notice Penalty to reduce chance as chance/delta if the hero not in his biome /// @dev Use StatLib.mintDropChanceDelta uint mintDropChanceDelta; /// @notice SCR-1064: drop chance depends on NG_LEVEL, decimals 18, value is in the range [0...1e18] /// it's always 100% for NG0 (no reduce, value is 1e18) /// Use {dropChancePercent} to calculate actual value uint mintDropChanceNgLevelMultiplier; } struct ItemWithId { address item; uint itemId; } struct SenderInfo { address msgSender; bool isEoa; } //endregion ------------------------ Data types //region ------------------------ Restrictions function onlyDeployer(IController c, address sender) internal view { if (!c.isDeployer(sender)) revert IAppErrors.ErrorNotDeployer(sender); } function onlyEOA(bool isEoa) internal view { if (!isEoa) { revert IAppErrors.NotEOA(msg.sender); } } function onlyStoryController(IController c, address sender) internal view { if (sender != c.storyController()) revert IAppErrors.ErrorNotStoryController(); } function onlyNotEquippedItem(IItemController.MainState storage s, address item, uint itemId) internal view { if (s.equippedOn[item.packNftId(itemId)] != bytes32(0)) revert IAppErrors.ItemEquipped(item, itemId); } function onlyNotConsumable(IItemController.ItemMeta memory meta, address item) internal pure { if ( uint(meta.itemType) == 0 || meta.itemMetaType == uint8(IItemController.ItemMetaType.CONSUMABLE) // todo probably first check is enough? ) revert IAppErrors.Consumable(item); } function checkPauseEoa(SenderInfo memory senderInfo, IController controller) internal view { onlyEOA(senderInfo.isEoa); if (controller.onPause()) revert IAppErrors.ErrorPaused(); } function onlyRegisteredControllers(ControllerContextLib.ControllerContext memory cc) internal view { if ( msg.sender != address(ControllerContextLib.dungeonFactory(cc)) && msg.sender != address(ControllerContextLib.reinforcementController(cc)) && msg.sender != address(ControllerContextLib.pvpController(cc)) ) revert IAppErrors.ErrorForbidden(msg.sender); } function checkRequirements( ControllerContextLib.ControllerContext memory cc, address hero, uint heroId, IStatController.CoreAttributes memory requirements ) internal view { IStatController.CoreAttributes memory attributes = ControllerContextLib.statController(cc).heroBaseAttributes(hero, heroId); if ( requirements.strength > attributes.strength || requirements.dexterity > attributes.dexterity || requirements.vitality > attributes.vitality || requirements.energy > attributes.energy ) revert IAppErrors.RequirementsToItemAttributes(); } /// @notice ensure that the user belongs to a guild, the guild has a shelter, the shelter has highest level 3 function _onlyMemberOfGuildWithShelterMaxLevel(ControllerContextLib.ControllerContext memory cc, address msgSender) internal view { // ensure that signer belongs to a guild and the guild has a shelter of ANY level IGuildController gc = ControllerContextLib.guildController(cc); if (address(gc) == address(0)) revert IAppErrors.NotInitialized(); uint guildId = gc.memberOf(msgSender); if (guildId == 0) revert IAppErrors.NotGuildMember(); uint shelterId = gc.guildToShelter(guildId); if (shelterId == 0) revert IAppErrors.GuildHasNoShelter(); // only highest level of shelters gives possibility to exit from dungeon (, uint8 shelterLevel,) = PackingLib.unpackShelterId(shelterId); if (shelterLevel != ShelterLib.MAX_SHELTER_LEVEL) revert IAppErrors.TooLowShelterLevel(shelterLevel, ShelterLib.MAX_SHELTER_LEVEL); } function onlyOwner(address token, uint tokenId, address sender) internal view { if (IERC721(token).ownerOf(tokenId) != sender) revert IAppErrors.ErrorNotOwner(token, tokenId); } function onlyAliveHero(ControllerContextLib.ControllerContext memory cc, address hero, uint heroId) internal view { if (!ControllerContextLib.statController(cc).isHeroAlive(hero, heroId)) revert IAppErrors.ErrorHeroIsDead(hero, heroId); } /// @notice Hero belongs to the {sender} and /// 1) in sandbox mode: the item belongs to the same hero /// 2) in not sandbox mode: the item either belongs to the same sender /// or the item is sandbox item that belongs to the hero 2 (upgraded) /// and the hero 2 belongs to the same sender /// @param sandboxMode Sandbox mode of the given hero /// @return inSandbox True if the item is located in some sandbox (the hero or some other hero) function onlyItemOwner( ControllerContextLib.ControllerContext memory cc, ItemWithId memory itemData, address hero, uint heroId, address msgSender, IHeroController.SandboxMode sandboxMode, bool checkIfHeroAlive ) internal view returns (bool inSandbox) { onlyOwner(hero, heroId, msgSender); if (checkIfHeroAlive) { ItemLib.onlyAliveHero(cc, hero, heroId); } bool checkSenderIsItemOwner; if (sandboxMode == IHeroController.SandboxMode.SANDBOX_MODE_1) { // the hero is in the sandbox mode, he can use only his own items from the sandbox inSandbox = _checkInSandbox(cc, itemData, hero, heroId); // not-upgraded hero has sandbox item outside of the sandbox .. it means the item is equipped on the hero if (!inSandbox) revert IAppErrors.SandboxItemAlreadyEquipped(); } else { // let's detect item owner (address hero2, uint heroId2) = ControllerContextLib.itemBoxController(cc).itemHero(itemData.item, itemData.itemId); if (hero2 == address(0)) { checkSenderIsItemOwner = true; } else if (hero2 == hero && heroId == heroId2) { inSandbox = _checkInSandbox(cc, itemData, hero, heroId); } else { IHeroController.SandboxMode sandboxMode2 = ItemLib._getSandboxMode(cc, hero2, heroId2); if (sandboxMode2 == IHeroController.SandboxMode.NORMAL_MODE_0) { checkSenderIsItemOwner = true; } else if (sandboxMode2 == IHeroController.SandboxMode.SANDBOX_MODE_1) { revert IAppErrors.SandboxModeNotAllowed(); } else { inSandbox = _checkInSandbox(cc, itemData, hero2, heroId2); if (inSandbox) { onlyOwner(hero2, heroId2, msgSender); } else { checkSenderIsItemOwner = true; } } } } if (checkSenderIsItemOwner) { onlyOwner(itemData.item, itemData.itemId, msgSender); } return inSandbox; } function _checkInSandbox( ControllerContextLib.ControllerContext memory cc, ItemWithId memory itemData, address hero, uint heroId ) internal view returns (bool) { IItemBoxController.ItemState itemState = ControllerContextLib.itemBoxController(cc).itemState(hero, heroId, itemData.item, itemData.itemId); if (itemState == IItemBoxController.ItemState.NOT_REGISTERED_0) revert IAppErrors.SandboxItemNotRegistered(); if (itemState == IItemBoxController.ItemState.NOT_AVAILABLE_1) revert IAppErrors.SandboxItemNotActive(); return itemState == IItemBoxController.ItemState.INSIDE_2; } /// @notice Either both items belong to the {sender} or both sandbox-items belong to the same hero of the given {sender} /// @return [item is in the sandbox, other item is in the sandbox] function _checkOwnerItems( ControllerContextLib.ControllerContext memory cc, ItemWithId memory item, ItemWithId memory otherItem, address sender ) internal view returns (bool[2] memory) { (address hero1, uint heroId1, IHeroController.SandboxMode sandboxMode1, bool inSandbox1) = _checkSingleItem(cc, item, sender); (address hero2, uint heroId2, IHeroController.SandboxMode sandboxMode2, bool inSandbox2) = _checkSingleItem(cc, otherItem, sender); if (sandboxMode1 == IHeroController.SandboxMode.SANDBOX_MODE_1 || sandboxMode2 == IHeroController.SandboxMode.SANDBOX_MODE_1) { if (hero1 != hero2 || heroId1 != heroId2) revert IAppErrors.SandboxDifferentHeroesNotAllowed(); } return [inSandbox1, inSandbox2]; } /// @dev a part of {_checkOwnerItems} function _checkSingleItem(ControllerContextLib.ControllerContext memory cc, ItemWithId memory item, address sender) internal view returns ( address hero, uint heroId, IHeroController.SandboxMode sandboxMode, bool inSandbox ) { (hero, heroId) = ControllerContextLib.itemBoxController(cc).itemHero(item.item, item.itemId); sandboxMode = hero == address(0) ? IHeroController.SandboxMode.NORMAL_MODE_0 : ItemLib._getSandboxMode(cc, hero, heroId); inSandbox = hero != address(0) && onlyItemOwner(cc, item, hero, heroId, sender, sandboxMode, true); if (hero == address(0)) { onlyOwner(item.item, item.itemId, sender); } else { onlyOwner(hero, heroId, sender); } } //endregion ------------------------ Restrictions //region ------------------------ Main logic /// @notice Mint new item, setup attributes, make extra setup if necessary (setup attack item, buff item) /// @param sender Dungeon Factory / User Controller / Guild Controller are allowed /// @param item Item to be minted /// @param recipient The item is minted for the given recipient /// @return itemId Id of the newly minted item function mintNewItem( IItemController.MainState storage s, IController controller, address sender, address item, address recipient ) external returns (uint itemId) { ControllerContextLib.ControllerContext memory ctx = ControllerContextLib.init(controller); address guildController = address(ControllerContextLib.guildController(ctx)); address shelterController = guildController == address(0) ? address(0) : IGuildController(guildController).shelterController(); if ( address(ControllerContextLib.dungeonFactory(ctx)) != sender && address(ControllerContextLib.userController(ctx)) != sender && guildController != sender && shelterController != sender && address(ControllerContextLib.itemController(ctx)) != sender && address(ControllerContextLib.heroController(ctx)) != sender ) revert IAppErrors.MintNotAllowed(); itemId = IItem(item).mintFor(recipient); IItemController.MintInfo memory info; ( info.meta, info.attributesIds, info.attributesValues, info.itemRarity ) = _setupNewAttributes(s, item, itemId, CalcLib.pseudoRandom); // setup extra info if (info.meta.itemMetaType == uint8(IItemController.ItemMetaType.ATTACK)) { info.attackInfo = unpackItemAttackInfo(_setupNewAttackItem(s, item, itemId)); } else if (info.meta.itemMetaType == uint8(IItemController.ItemMetaType.BUFF)) { ( info.casterIds, info.casterValues, info.targetIds, info.targetValues ) = _setupNewBuffItem(s, item, itemId, CalcLib.pseudoRandom); } // consumable stats unchangeable, get them by address emit IApplicationEvents.NewItemMinted(item, itemId, info); } /// @notice Mint random items, not more than {info.maxItems} function mintRandomItems(MintItemInfo memory info) internal returns (address[] memory) { return _mintRandomItems(info, CalcLib.nextPrng); } function applyActionMasks( uint actionMask, IStatController statController, address heroToken, uint heroTokenId ) external { if ((actionMask & (2 ** uint(IItemController.ConsumableActionBits.CLEAR_TEMPORARY_ATTRIBUTES_0))) != 0) { statController.clearTemporallyAttributes(heroToken, heroTokenId); } } function destroy(IItemController.MainState storage s, IController controller, address msgSender, address item, uint itemId) external { address owner = IERC721(item).ownerOf(itemId); address itemBox = controller.itemBoxController(); bool sandbox = owner == itemBox; if ( controller.gameObjectController() != msgSender && controller.storyController() != msgSender ) { if (sandbox) { (address hero, uint heroId) = IItemBoxController(itemBox).itemHero(item, itemId); address heroOwner = IERC721(hero).ownerOf(heroId); if (heroOwner != msgSender) revert IAppErrors.ErrorForbidden(msgSender); } else { if (owner != msgSender) revert IAppErrors.ErrorForbidden(msgSender); } } ItemLib.onlyNotEquippedItem(s, item, itemId); ControllerContextLib.ControllerContext memory cc = ControllerContextLib.init(controller); _destroy(cc, item, itemId, sandbox); } //endregion ------------------------ Main logic //region ------------------------ Internal logic function _destroy(ControllerContextLib.ControllerContext memory cc, address item, uint itemId, bool inSandbox) internal { if (inSandbox) { ControllerContextLib.itemBoxController(cc).destroyItem(item, itemId); } else { IItem(item).burn(itemId); } emit IApplicationEvents.Destroyed(item, itemId); } /// @param nextPrng_ CalcLib.nextPrng, param is required by unit tests function _mintRandomItems( MintItemInfo memory info, function (LibPRNG.PRNG memory, uint) internal view returns (uint) nextPrng_ ) internal returns (address[] memory) { // if hero is not in his biome do not mint at all if (info.mintDropChanceDelta != 0) { return new address[](0); } uint len = info.mintItems.length; // Fisher–Yates shuffle LibPRNG.PRNG memory prng = LibPRNG.PRNG(info.oracle.getRandomNumber(CalcLib.MAX_CHANCE, info.seed)); uint[] memory indices = new uint[](len); for (uint i = 1; i < len; ++i) { indices[i] = i; } LibPRNG.shuffle(prng, indices); address[] memory minted = new address[](len); uint mintedLength; uint di = Math.min(CalcLib.toUint(info.destroyItems), 100); for (uint i; i < len; ++i) { if (info.mintItemsChances[indices[i]] > CalcLib.MAX_CHANCE) { revert IAppErrors.TooHighChance(info.mintItemsChances[indices[i]]); } uint chance = _adjustChance(info.mintItemsChances[indices[i]], info, di); // need to call random in each loop coz each minted item should have dedicated chance uint rnd = nextPrng_(prng, CalcLib.MAX_CHANCE); // randomWithSeed_(CalcLib.MAX_CHANCE, rndSeed); if (chance != 0 && (chance >= CalcLib.MAX_CHANCE || rnd < chance)) { // There is no break here: the cycle is continued even if the number of the minted items reaches the max. // The reason: gas consumption of success operation must be great of equal of the gas consumption of fail op. if (mintedLength < info.maxItems) { minted[i] = info.mintItems[indices[i]]; ++mintedLength; } } } address[] memory mintedAdjusted = new address[](mintedLength); uint j; for (uint i; i < len; ++i) { if (minted[i] != address(0)) { mintedAdjusted[j] = minted[i]; ++j; } } return mintedAdjusted; } /// @notice Apply all corrections to the chance of item drop /// There are two params to increase chances: amplifier and magicFind /// There are two params to decrease chances: destroyItems and mintDropChanceNgLevelMultiplier /// @param info Assume here, that info.mintDropChanceNgLevelMultiplier is in the range [0..1e18] /// @param di Assume that di <= 100 function _adjustChance(uint32 itemChance, MintItemInfo memory info, uint di) internal pure returns (uint) { uint chance = uint(itemChance) * Math.min(1e18, info.mintDropChanceNgLevelMultiplier) / 1e18; chance += chance * info.amplifier / StatLib._MAX_AMPLIFIER; chance += chance * CalcLib.toUint(info.magicFind) / 100; chance -= chance * di / 100; return chance; } function _setupNewAttributes( IItemController.MainState storage s, address item, uint itemId, function (uint) internal view returns (uint) random_ ) internal returns ( IItemController.ItemMeta memory meta, uint8[] memory ids, int32[] memory values, IItemController.ItemRarity itemRarity ){ meta = unpackedItemMeta(s.itemMeta[item]); (ids, values, itemRarity) = _generateAttributes(unpackItemGenerateInfo(s.generateInfoAttributes[item]), meta, random_); bytes32 packedItemId = item.packNftId(itemId); if (ids.length != 0) { s._itemAttributes[packedItemId] = values.toBytes32ArrayWithIds(ids); } s.itemInfo[packedItemId] = PackingLib.packItemInfo(uint8(itemRarity), 0, meta.baseDurability); } function _setupNewAttackItem(IItemController.MainState storage s, address item, uint itemId) internal returns (bytes32 attackInfo){ // we just write data for attack item, no need to generate, it will be augmented later so need individual data for itemId attackInfo = s.generateInfoAttack[item]; s._itemAttackInfo[item.packNftId(itemId)] = attackInfo; } function _setupNewBuffItem( IItemController.MainState storage s, address item, uint itemId, function (uint) internal view returns (uint) random_ ) internal returns ( uint8[] memory casterIds, int32[] memory casterValues, uint8[] memory targetIds, int32[] memory targetValues ){ // CASTER (casterIds, casterValues) = _generateSimpleAttributes( unpackItemGenerateInfo(s.generateInfoCasterAttributes[item]), true, random_ ); if (casterIds.length != 0) { s._itemCasterAttributes[item.packNftId(itemId)] = casterValues.toBytes32ArrayWithIds(casterIds); } // TARGET (targetIds, targetValues) = _generateSimpleAttributes( unpackItemGenerateInfo(s.generateInfoTargetAttributes[item]), true, random_ ); if (targetIds.length != 0) { s._itemTargetAttributes[item.packNftId(itemId)] = targetValues.toBytes32ArrayWithIds(targetIds); } } /// @notice Generate all mandatory attributes and try to generate required number of random attributes. /// Generate at least {info.minRandomAttributes} of random attributes if it's possible /// but not more than {info.maxRandomAttributes}. Value of each attribute is generated randomly according its chances. /// @param meta Assume, that meta.min != 0, meta.max != 0 and both meta.min and meta.min should have same sign /// because results value cannot be 0 /// @return ids Ids of the attributes, zero id is allowed /// @return values Randomly generated attributes values, min <= value <= max /// @return itemRarity Rarity of the item (Either meta.defaultRarity or calculated if there is no default rarity) function _generateAttributes( IItemController.ItemGenerateInfo memory info, IItemController.ItemMeta memory meta, function (uint) internal view returns (uint) random_ ) internal view returns ( uint8[] memory ids, int32[] memory values, IItemController.ItemRarity itemRarity ) { GenerateAttributesContext memory ctx; uint len = info.ids.length; if (len != 0) { ctx.ids = new uint8[](len); ctx.values = new int32[](len); ctx.usedIndexes = new bool[](len); // Fisher–Yates shuffle _shuffleInfo(info, random_); // initialize ctx by initial values // generate all mandatory attributes, try to generate not more than {meta.maxRandomAttributes} random attributes _prepareAttributes(info, meta.maxRandomAttributes, ctx, random_); // generate missing random attributes if it's necessary, ctx.counter is incremented _generateMissingRandomAttributes(info, meta.minRandomAttributes, ctx, random_); itemRarity = meta.defaultRarity == 0 ? _calculateRarity(ctx.randomSum, ctx.chancesSum, ctx.randomAttrCounter, meta.maxRandomAttributes) : IItemController.ItemRarity(meta.defaultRarity); } else { itemRarity = IItemController.ItemRarity.UNKNOWN; } (ids, values) = _fixLengthsIdsValues(ctx.ids, ctx.values, ctx.counter); } /// @notice Generate missing random attributes if necessary function _generateMissingRandomAttributes( IItemController.ItemGenerateInfo memory info, uint8 minRandomAttributes, GenerateAttributesContext memory ctx, function (uint) internal view returns (uint) random_ ) internal view { uint attrToGen = Math.min(ctx.totalRandomAttrsPossible, minRandomAttributes); if (ctx.randomAttrCounter < attrToGen && ctx.totalRandomAttrsPossible > ctx.randomAttrCounter) { // it's necessary AND possible to generate more random attributes uint possibleRemainingAttrs = ctx.totalRandomAttrsPossible - ctx.randomAttrCounter; uint remainingAttrsToGen = attrToGen - ctx.randomAttrCounter; uint[] memory indicesToGen = new uint[](possibleRemainingAttrs); uint indicesToGenCounter; // enumerate all attributes, add all indices of not-generated attributes to {indexesToGen} for (uint i; i < info.ids.length; ++i) { // mandatory attrs should be already generated and no need to check if (!ctx.usedIndexes[i]) { indicesToGen[indicesToGenCounter] = i; indicesToGenCounter++; } } // Shuffle indices of not-generated attributes using Fisher–Yates shuffle if (possibleRemainingAttrs > 1) { for (uint i; i < possibleRemainingAttrs - 1; ++i) { uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, possibleRemainingAttrs - 1, random_); (indicesToGen[randomIndex], indicesToGen[i]) = (indicesToGen[i], indicesToGen[randomIndex]); } } // Generate necessary amount of attributes. Fist (shuffled) attributes are selected (MAX_CHANCE is used for each) for (uint i; i < remainingAttrsToGen; ++i) { uint idx = indicesToGen[i]; (int32 attr,) = _generateAttribute(info.mins[idx], info.maxs[idx], CalcLib.MAX_CHANCE, random_); ctx.ids[ctx.counter] = info.ids[idx]; ctx.values[ctx.counter] = attr; ctx.counter++; } } } /// @notice Generate all mandatory attributes, generate not more than {meta.maxRandomAttributes} random attributes. /// Updates context: /// {ctx.totalRandomAttrsPossible} - total number of possible random attributes /// {ctx.randomAttrCounter} - total number of generated random attributes <= {maxRandomAttributes} /// {ctx.randomSum} = sum of random of all random attributes. /// {ctx.chancesSum} = sum of chances of all random attributes. /// {ctx.counter} = total number of generated attributes. Values of ctx.ids, ctx.values, ctx.usedIndexes are /// initialized in the range [0...ctx.counter) /// @param ctx Empty struct but arrays ids, values and usedIndexes should be allocated for info.ids.length items function _prepareAttributes( IItemController.ItemGenerateInfo memory info, uint8 maxRandomAttributes, GenerateAttributesContext memory ctx, function (uint) internal view returns (uint) random_ ) internal view { uint len = info.ids.length; for (uint i; i < len; ++i) { if (info.chances[i] != CalcLib.MAX_CHANCE) { ctx.totalRandomAttrsPossible++; } if (info.chances[i] >= CalcLib.MAX_CHANCE || !ctx.stopGenerateRandom) { (int32 attr, uint random) = _generateAttribute(info.mins[i], info.maxs[i], info.chances[i], random_); // count only random attributes for calc rarity if (attr != 0) { if ( info.chances[i] < CalcLib.MAX_CHANCE // && random != 0 // commented: random = 0 can produce crash in _generateMissingRandomAttributes ) { ctx.randomAttrCounter++; ctx.randomSum += random; ctx.chancesSum += info.chances[i]; } ctx.ids[ctx.counter] = info.ids[i]; ctx.values[ctx.counter] = attr; ctx.counter++; ctx.usedIndexes[i] = true; } // it is a bit less fair random for attrs in the end of the list, however we assume it should be pretty rare case if (ctx.randomAttrCounter == maxRandomAttributes) { ctx.stopGenerateRandom = true; } } } } /// @notice Shuffle info arrays using Fisher–Yates shuffle algo function _shuffleInfo( IItemController.ItemGenerateInfo memory info, function (uint) internal view returns (uint) random_ ) internal view { uint len = info.ids.length; if (len > 1) { for (uint i; i < len - 1; i++) { uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, len - 1, random_); (info.ids[randomIndex], info.ids[i]) = (info.ids[i], info.ids[randomIndex]); (info.mins[randomIndex], info.mins[i]) = (info.mins[i], info.mins[randomIndex]); (info.maxs[randomIndex], info.maxs[i]) = (info.maxs[i], info.maxs[randomIndex]); (info.chances[randomIndex], info.chances[i]) = (info.chances[i], info.chances[randomIndex]); } } } /// @notice Generate array [0,1,2.. N-1] and shuffle it using Fisher–Yates shuffle algo function _shuffleIndices( uint countItems, function (uint) internal view returns (uint) random_ ) internal view returns (uint[] memory indices){ indices = new uint[](countItems); for (uint i = 1; i < countItems; ++i) { indices[i] = i; } if (countItems > 1) { for (uint i; i < countItems - 1; i++) { uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, countItems - 1, random_); (indices[randomIndex], indices[i]) = (indices[i], indices[randomIndex]); } } } /// @notice Reduce lengths of {ids} and {values} to {count} function _fixLengthsIdsValues(uint8[] memory ids, int32[] memory values, uint count) internal pure returns ( uint8[] memory idsOut, int32[] memory valuesOut ) { if (count == ids.length) { return (ids, values); } idsOut = new uint8[](count); valuesOut = new int32[](count); for (uint i; i < count; ++i) { idsOut[i] = ids[i]; valuesOut[i] = values[i]; } return (idsOut, valuesOut); } /// @param random_ Pass CalcLib.pseudoRandom here, param is required for unit tests. Max value is MAX_CHANCE function _generateSimpleAttributes( IItemController.ItemGenerateInfo memory info, bool maxChance, function (uint) internal view returns (uint) random_ ) internal view returns ( uint8[] memory ids, int32[] memory values ) { uint len = info.ids.length; ids = new uint8[](len); values = new int32[](len); uint n = 0; for (uint i; i < len; ++i) { (int32 attr,) = _generateAttribute( info.mins[i], info.maxs[i], maxChance ? CalcLib.MAX_CHANCE : info.chances[i], random_ ); if (attr != 0) { ids[n] = info.ids[i]; values[n] = attr; ++n; } } return _fixLengthsIdsValues(ids, values, n); } //endregion ------------------------ Internal logic //region ------------------------ Internal utils /// @param chance Chance in the range [0...MAX_CHANCE], MAX_CHANCE=1e9 means "mandatory" element. /// @param random_ Pass CalcLib.pseudoRandom here, param is required for unit tests /// @return attr Either 0 or min <= attr <= max /// @return rnd Random value in the range [0...MAX_CHANCE]; It's always 0 for mandatory elements function _generateAttribute( int32 min, int32 max, uint32 chance, function (uint) internal view returns (uint) random_ ) internal view returns ( int32 attr, uint rnd ) { if (chance > CalcLib.MAX_CHANCE) revert IAppErrors.TooHighChance(chance); uint diff = uint(CalcLib.absDiff(min, max)); if (chance < CalcLib.MAX_CHANCE) { uint32 random = CalcLib.pseudoRandomUint32Flex(CalcLib.MAX_CHANCE, random_); if (random < chance) { uint r = uint(CalcLib.MAX_CHANCE - random * (CalcLib.MAX_CHANCE / chance)); int32 k = int32(int(r * diff / uint(CalcLib.MAX_CHANCE))); return (min + k, random); } } else { // chance == CalcLib.MAX_CHANCE => mandatory element if (diff == 0) { return (min, 0); } else { uint r = uint(CalcLib.pseudoRandomUint32Flex(CalcLib.MAX_CHANCE, random_)); int32 k = int32(int(r % (diff + 1))); // return zero random - no need to calc rarity for mandatory elements return (min + k, 0); } } return (0, 0); } /// @notice Calculate item rarity /// @param randomSum Total sum random values of all random attributes in ItemGenerateInfo, [0...MAX_CHANCE/attrCounter] /// @param chancesSum Total sum of all random chances in ItemGenerateInfo /// @param attrCounter Count of random attributes in ItemGenerateInfo /// @param maxAttr Index of max allowed random attribute (all attributes with higher indices are not random) /// @return item rarity function _calculateRarity(uint randomSum, uint chancesSum, uint attrCounter, uint maxAttr) internal pure returns ( IItemController.ItemRarity ) { if (attrCounter == 0) { return IItemController.ItemRarity.NORMAL; } uint random = randomSum / attrCounter; uint averageChance = chancesSum / attrCounter; if (random > CalcLib.MAX_CHANCE) revert IAppErrors.TooHighRandom(random); if (random < averageChance / 4 && attrCounter == maxAttr) { return IItemController.ItemRarity.RARE; } else if (random < averageChance * 3 / 4) { return attrCounter > 2 ? IItemController.ItemRarity.RARE : IItemController.ItemRarity.MAGIC; } else { return attrCounter > 1 ? IItemController.ItemRarity.MAGIC : IItemController.ItemRarity.NORMAL; } } function _getSandboxMode(ControllerContextLib.ControllerContext memory cc, address hero, uint heroId) internal view returns ( IHeroController.SandboxMode sandboxMode ) { return IHeroController.SandboxMode(ControllerContextLib.heroController(cc).sandboxMode(hero, heroId)); } //endregion ------------------------ Internal utils //region ------------------------ PACKING function packItemGenerateInfo(IItemController.ItemGenerateInfo memory info) internal pure returns (bytes32[] memory result) { uint len = info.ids.length; if (len != info.mins.length || len != info.maxs.length || len != info.chances.length) { revert IAppErrors.LengthsMismatch(); } result = new bytes32[](len); for (uint i; i < len; ++i) { result[i] = PackingLib.packItemGenerateInfo(info.ids[i], info.mins[i], info.maxs[i], info.chances[i]); } } function unpackItemGenerateInfo(bytes32[] memory gen) internal pure returns ( IItemController.ItemGenerateInfo memory ) { uint length = gen.length; uint8[] memory ids = new uint8[](length); int32[] memory mins = new int32[](length); int32[] memory maxs = new int32[](length); uint32[] memory chances = new uint32[](length); for (uint i; i < length; ++i) { (ids[i], mins[i], maxs[i], chances[i]) = gen[i].unpackItemGenerateInfo(); } return IItemController.ItemGenerateInfo(ids, mins, maxs, chances); } function packItemMeta(IItemController.ItemMeta memory meta) internal pure returns (bytes32) { return PackingLib.packItemMeta( meta.itemMetaType, meta.itemLevel, uint8(meta.itemType), meta.baseDurability, meta.defaultRarity, meta.minRandomAttributes, meta.maxRandomAttributes, meta.manaCost, meta.requirements ); } function unpackedItemMeta(bytes32 meta) internal pure returns (IItemController.ItemMeta memory result) { return meta.unpackItemMeta(); } function packItemInfo(IItemController.ItemInfo memory info) internal pure returns (bytes32) { return PackingLib.packItemInfo(uint8(info.rarity), info.augmentationLevel, info.durability); } function unpackedItemInfo(bytes32 info) internal pure returns (IItemController.ItemInfo memory result) { uint8 rarity; (rarity, result.augmentationLevel, result.durability) = info.unpackItemInfo(); result.rarity = IItemController.ItemRarity(rarity); return result; } function packItemAttackInfo(IItemController.AttackInfo memory info) internal pure returns (bytes32) { return PackingLib.packItemAttackInfo( uint8(info.aType), info.min, info.max, info.attributeFactors.strength, info.attributeFactors.dexterity, info.attributeFactors.vitality, info.attributeFactors.energy ); } function unpackItemAttackInfo(bytes32 info) internal pure returns (IItemController.AttackInfo memory result) { IStatController.CoreAttributes memory fs; uint8 aType; (aType, result.min, result.max, fs.strength, fs.dexterity, fs.vitality, fs.energy) = info.unpackItemAttackInfo(); result.aType = IItemController.AttackType(aType); result.attributeFactors = fs; return result; } //endregion ------------------------ PACKING }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IGOC.sol"; import "./CalcLib.sol"; import "./PackingLib.sol"; import "./StatLib.sol"; import "./ItemLib.sol"; import "./StringLib.sol"; import "./FightLib.sol"; import "./RewardsPoolLib.sol"; import "../interfaces/IController.sol"; import "../interfaces/IStatController.sol"; import "../interfaces/IOracle.sol"; import "../interfaces/IFightCalculator.sol"; import "../interfaces/IDungeonFactory.sol"; import "../interfaces/IItemController.sol"; import "../interfaces/IERC20.sol"; library MonsterLib { using CalcLib for int32; using PackingLib for bytes32; using PackingLib for bytes32[]; using PackingLib for uint16; using PackingLib for uint8; using PackingLib for address; using PackingLib for uint32[]; using PackingLib for uint32; using PackingLib for uint64; using PackingLib for int32[]; using PackingLib for int32; /// @notice Max value for monster rarity and monster/dungeon multiplier uint32 internal constant _MAX_AMPLIFIER = 1e9; uint private constant _TOTAL_SUPPLY_BASE = 10_000_000e18; /// @notice Base monster multiplier for NG+. Multiplier = base multiplier * hero ng_level uint internal constant _MONSTER_MULTIPLIER_NGP_BASE = uint(_MAX_AMPLIFIER); //region ------------------------ Data types struct AdrContext { address sender; address heroToken; IController controller; IOracle oracle; IStatController statController; IItemController itemController; uint heroTokenId; } struct FightInternalInfo { int32 manaConsumed; int32 damage; int32 heroLifeRegen; int32 heroHp; int32 monsterHp; uint32 monsterRarity; IFightCalculator.FighterInfo heroFightInfo; IFightCalculator.FighterInfo monsterFightInfo; } //endregion ------------------------ Data types //region ------------------------ Main logic /// @param heroNgLevel Pass type(uint8).max for !NG+ function initialGeneration( IGOC.MonsterInfo storage mInfo, address heroToken, uint heroTokenId, uint iteration, uint8 heroNgLevel ) internal { return _initialGeneration(mInfo, heroToken, heroTokenId, iteration, _pseudoRandom, heroNgLevel); } /// @notice Fight, post fight, generate fight results /// @return result Fields objectId, heroToken, heroTokenId, iteration remain uninitialized here. /// Caller is responsible to set that values. /// @dev weird, but memory ctx is more efficient here than calldata ctx function action(IGOC.ActionContext memory ctx, IGOC.MonsterInfo storage mInfo) external returns ( IGOC.ActionResult memory result, uint8 turn ) { return _action(ctx, mInfo, _pseudoRandom, FightLib.fight); } //endregion ------------------------ Main logic //region ------------------------ Internal calculations function _action( IGOC.ActionContext memory ctx, IGOC.MonsterInfo storage mInfo, function (uint) internal view returns (uint) random_, function( IItemController, IFightCalculator.FightCall memory, IFightCalculator.FightCallAdd memory, function (uint) internal view returns (uint) ) internal returns (IFightCalculator.FightResult memory) fight_ ) internal returns ( IGOC.ActionResult memory result, uint8 turn ) { AdrContext memory adrCtx = _context(ctx); IGOC.GeneratedMonster memory gen = unpackGeneratedMonster(mInfo._generatedMonsters[ctx.heroToken.packNftId(ctx.heroTokenId)][ctx.iteration]); turn = gen.turnCounter; (FightInternalInfo memory fInfo, IGOC.MonsterGenInfo memory genInfo) = _fight(ctx, mInfo, gen, adrCtx, random_, fight_); result = _postFight(mInfo, ctx, adrCtx, fInfo, genInfo, gen); } /// @dev This function was extracted from {action()} to simplify unit testing /// @param gen These values CAN BE modified in place in some cases. /// @return result Fields objectId, heroToken, heroTokenId, iteration remain uninitialized here. /// Caller is responsible to set that values. function _postFight( IGOC.MonsterInfo storage mInfo, IGOC.ActionContext memory ctx, AdrContext memory adrCtx, FightInternalInfo memory fInfo, IGOC.MonsterGenInfo memory genInfo, IGOC.GeneratedMonster memory gen ) internal returns ( IGOC.ActionResult memory result ) { bytes32 heroPackedId = ctx.heroToken.packNftId(ctx.heroTokenId); if (gen.turnCounter > 100) { // instant kill hero if too long battle fInfo.heroHp = 0; } bool isMonsterDead = fInfo.monsterHp == 0; bool isHeroDead = fInfo.heroHp == 0; if (isMonsterDead) { _bossDefeated(adrCtx, ctx); } if (isMonsterDead || isHeroDead) { if (gen.generated) { delete mInfo._generatedMonsters[heroPackedId][ctx.iteration]; } // assume that if the hero is dead clearUsedConsumables will be called in _objectAction if (isMonsterDead) { adrCtx.statController.clearUsedConsumables(ctx.heroToken, ctx.heroTokenId); } } else { if (gen.generated) { gen.hp = fInfo.monsterHp; gen.turnCounter = gen.turnCounter + 1; } else { // new instance of gen is created gen = IGOC.GeneratedMonster({ generated: true, amplifier: fInfo.monsterRarity, hp: fInfo.monsterHp, turnCounter: 1 }); } mInfo._generatedMonsters[heroPackedId][ctx.iteration] = packGeneratedMonster(gen); } if (isMonsterDead) { bytes32 index = _getMonsterCounterIndex(ctx.objectId); uint curValue = adrCtx.statController.heroCustomData(ctx.heroToken, ctx.heroTokenId, index); adrCtx.statController.setHeroCustomData(ctx.heroToken, ctx.heroTokenId, index, curValue + 1); } // --- generate result result.kill = isHeroDead; result.experience = isMonsterDead ? StatLib.expPerMonster( fInfo.monsterFightInfo.fighterStats.experience, fInfo.monsterRarity, fInfo.heroFightInfo.fighterStats.experience, fInfo.heroFightInfo.fighterStats.level, ctx.biome ) : 0; result.heal = fInfo.heroLifeRegen; result.manaRegen = isMonsterDead ? fInfo.heroFightInfo.fighterAttributes[uint(IStatController.ATTRIBUTES.MANA_AFTER_KILL)] : int32(0); // result.lifeChancesRecovered = 0; // zero by default result.damage = fInfo.damage; result.manaConsumed = fInfo.manaConsumed; result.mintItems = isMonsterDead ? _mintRandomItems(fInfo, ctx, genInfo, CalcLib.nextPrng) : new address[](0); result.completed = isMonsterDead || isHeroDead; return result; } /// @notice Generate new {GeneratedMonster} and put it to {mInfo._generatedMonsters} /// @param random_ Pass _pseudoRandom here, param is required for unit tests, range [0...MAX_AMPLIFIER] /// @param heroNgLevel Assume type(uint8).max for !NG+ function _initialGeneration( IGOC.MonsterInfo storage mInfo, address heroToken, uint heroTokenId, uint iteration, function (uint) internal view returns (uint) random_, uint8 heroNgLevel ) internal { IGOC.GeneratedMonster memory gen = IGOC.GeneratedMonster({ generated: true, amplifier: uint32(random_(_MAX_AMPLIFIER)), hp: 0, turnCounter: 0 }); IGOC.MonsterGenInfo memory info = unpackMonsterInfo(mInfo); (int32[] memory attributes,) = generateMonsterAttributes( info.attributeIds, info.attributeValues, gen.amplifier, monsterMultiplier(heroNgLevel), info.experience ); gen.hp = attributes[uint(IStatController.ATTRIBUTES.LIFE)]; mInfo._generatedMonsters[heroToken.packNftId(heroTokenId)][iteration] = packGeneratedMonster(gen); } function _bossDefeated(AdrContext memory adrCtx, IGOC.ActionContext memory ctx) internal { if (ctx.objectSubType == uint8(IGOC.ObjectSubType.BOSS_3)) { IDungeonFactory(adrCtx.controller.dungeonFactory()).setBossCompleted(ctx.objectId, ctx.heroToken, ctx.heroTokenId, ctx.biome); } } function _collectHeroFighterInfo( IFightCalculator.AttackInfo memory attackInfo, AdrContext memory adrContext ) internal view returns ( IFightCalculator.FighterInfo memory fInfo, int32 manaConsumed ) { IStatController.ChangeableStats memory heroStats = adrContext.statController.heroStats(adrContext.heroToken, adrContext.heroTokenId); (int32[] memory heroAttributes, int32 _manaConsumed) = _buffAndGetHeroAttributes( heroStats.level, attackInfo.skillTokens, attackInfo.skillTokenIds, adrContext.statController, adrContext.heroToken, adrContext.heroTokenId ); manaConsumed = _manaConsumed; if (attackInfo.attackType == IFightCalculator.AttackType.MAGIC) { manaConsumed += int32(adrContext.itemController.itemMeta(attackInfo.attackToken).manaCost); } fInfo = IFightCalculator.FighterInfo({ fighterAttributes: heroAttributes, fighterStats: heroStats, attackType: attackInfo.attackType, attackToken: attackInfo.attackToken, attackTokenId: attackInfo.attackTokenId, race: uint(IStatController.Race.HUMAN) }); } function _buffAndGetHeroAttributes( uint level, address[] memory skillTokens, uint[] memory skillTokenIds, IStatController statController, address heroToken, uint heroTokenId ) internal view returns ( int32[] memory heroAttributes, int32 manaConsumed ) { return statController.buffHero(IStatController.BuffInfo({ heroToken: heroToken, heroTokenId: heroTokenId, heroLevel: uint32(level), buffTokens: skillTokens, buffTokenIds: skillTokenIds })); } /// @notice Get skill tokens, ensure that they are equipped on, add skill-tokens target attributes to hero attributes /// @param attributes Hero attributes. These values are incremented in place // @param heroAttackInfo Checked attack info. Assume that all skill tokens belong either to the hero or to the helper. function _debuff( int32[] memory attributes, IFightCalculator.AttackInfo memory attackInfo, IItemController itemController ) internal view { uint length = attackInfo.skillTokens.length; for (uint i; i < length; ++i) { (int32[] memory values, uint8[] memory ids) = itemController.targetAttributes(attackInfo.skillTokens[i], attackInfo.skillTokenIds[i]); StatLib.attributesAdd(attributes, StatLib.valuesToFullAttributesArray(values, ids)); } } /// @param random_ Pass _pseudoRandom here, param is required for unit tests, range [0...MAX_AMPLIFIER] function _collectMonsterFighterInfo( IGOC.MultiplierInfo memory multiplierInfo, IGOC.MonsterInfo storage mInfo, IGOC.GeneratedMonster memory gen, IFightCalculator.AttackInfo memory heroAttackInfo, uint heroLevel, AdrContext memory adrCtx, function (uint) internal view returns (uint) random_ ) internal view returns ( IFightCalculator.FighterInfo memory fighterInfo, uint32 rarity, IGOC.MonsterGenInfo memory genInfo ) { IFightCalculator.AttackInfo memory attackInfo; rarity = gen.generated ? gen.amplifier : uint32(random_(_MAX_AMPLIFIER)); ( fighterInfo.fighterAttributes, fighterInfo.fighterStats.level, fighterInfo.fighterStats.experience, attackInfo, genInfo ) = _generateMonsterInfo( mInfo, rarity, monsterMultiplier(multiplierInfo.heroNgLevel), heroLevel, multiplierInfo.biome, random_ ); _debuff(fighterInfo.fighterAttributes, heroAttackInfo, adrCtx.itemController); fighterInfo.fighterStats.life = gen.generated ? uint32(gen.hp) : fighterInfo.fighterStats.life = uint32(CalcLib.max32(fighterInfo.fighterAttributes[uint(IStatController.ATTRIBUTES.LIFE)], int32(1))); fighterInfo.fighterStats.mana = uint32(fighterInfo.fighterAttributes[uint(IStatController.ATTRIBUTES.MANA)]); fighterInfo.attackType = attackInfo.attackType; fighterInfo.attackToken = attackInfo.attackToken; fighterInfo.attackTokenId = attackInfo.attackTokenId; fighterInfo.race = genInfo.race; return (fighterInfo, rarity, genInfo); } /// @notice Fight between the hero and the monster /// @param random_ Pass _pseudoRandom here, param is required to simplify unit testing /// @param fight_ Pass FightLib.fight here, param is required to simplify unit testing function _fight( IGOC.ActionContext memory ctx, IGOC.MonsterInfo storage mInfo, IGOC.GeneratedMonster memory gen, AdrContext memory adrCtx, function (uint) internal view returns (uint) random_, function( IItemController, IFightCalculator.FightCall memory, IFightCalculator.FightCallAdd memory, function (uint) internal view returns (uint) ) internal returns (IFightCalculator.FightResult memory) fight_ ) internal returns ( FightInternalInfo memory fInfo, IGOC.MonsterGenInfo memory info ) { IFightCalculator.FighterInfo memory heroFightInfo; IFightCalculator.FighterInfo memory monsterFightInfo; { IFightCalculator.AttackInfo memory heroAttackInfo = decodeAndCheckAttackInfo( adrCtx.itemController, IHeroController(IController(adrCtx.controller).heroController()), ctx.data, adrCtx.heroToken, adrCtx.heroTokenId ); // use fInfo.manaConsumed and fInfo.monsterRarity to story values temporally to avoid creation of additional vars (heroFightInfo, fInfo.manaConsumed) = _collectHeroFighterInfo(heroAttackInfo, adrCtx); (monsterFightInfo, fInfo.monsterRarity, info) = _collectMonsterFighterInfo( IGOC.MultiplierInfo({ biome: ctx.biome, heroNgLevel: ctx.heroNgLevel }), mInfo, gen, heroAttackInfo, heroFightInfo.fighterStats.level, adrCtx, random_ ); } // >>> FIGHT! IFightCalculator.FightResult memory fightResult = fight_( adrCtx.itemController, IFightCalculator.FightCall({ fighterA: heroFightInfo, fighterB: monsterFightInfo, dungeonId: ctx.dungeonId, objectId: ctx.objectId, heroAdr: adrCtx.heroToken, heroId: adrCtx.heroTokenId, stageId: ctx.stageId, iteration: ctx.iteration, turn: gen.turnCounter }), IFightCalculator.FightCallAdd({ msgSender: ctx.sender, fightId: 0 }), random_ ); fInfo = FightInternalInfo({ manaConsumed: fInfo.manaConsumed + fightResult.manaConsumedA, monsterRarity: fInfo.monsterRarity, damage: _calcDmg(int32(heroFightInfo.fighterStats.life), fightResult.healthA), heroFightInfo: heroFightInfo, monsterFightInfo: monsterFightInfo, heroLifeRegen: fightResult.healthA > int32(heroFightInfo.fighterStats.life) ? fightResult.healthA - int32(heroFightInfo.fighterStats.life) : int32(0), heroHp: fightResult.healthA, monsterHp: fightResult.healthB }); } /// @param random_ Pass _pseudoRandom here, param is required for unit tests, range [0...1e18] /// @return attributes Attributes amplified on amplifier and dungeonMultiplier /// @return level Result level in the range: [mInfo.level .. heroLevel] /// @return experience Experience amplified on amplifier and dungeonMultiplier /// @return attackInfo Attack info. For magic hero attack type monster will have melee in half hits (randomly) /// @return info Unpacked data from {mInfo}, some fields can be uninitialized, see comments to unpackMonsterInfo (!) function _generateMonsterInfo( IGOC.MonsterInfo storage mInfo, uint32 amplifier, uint dungeonMultiplier, uint heroLevel, uint biome, function (uint) internal view returns (uint) random_ ) internal view returns ( int32[] memory attributes, uint32 level, uint32 experience, IFightCalculator.AttackInfo memory attackInfo, IGOC.MonsterGenInfo memory info ) { info = unpackMonsterInfo(mInfo); level = uint32(info.level); if (level < heroLevel + 1) { level = uint32(Math.min(level + ((heroLevel - level) * 10 / 15), biome * 5)); } if (info.attackType == uint8(IFightCalculator.AttackType.MAGIC)) { // sometimes use melee (25% chance) uint rnd = random_(1e18); if (rnd > 0.75e18) { attackInfo.attackType = IFightCalculator.AttackType.MELEE; } else { attackInfo.attackType = IFightCalculator.AttackType.MAGIC; attackInfo.attackToken = info.attackToken; attackInfo.attackTokenId = info.attackTokenId; } } else { attackInfo.attackType = IFightCalculator.AttackType(info.attackType); } (attributes, experience) = generateMonsterAttributes( info.attributeIds, info.attributeValues, amplifier, dungeonMultiplier, info.experience ); return (attributes, level, experience, attackInfo, info); } function _mintRandomItems( FightInternalInfo memory fInfo, IGOC.ActionContext memory ctx, IGOC.MonsterGenInfo memory genInfo, function (LibPRNG.PRNG memory, uint) internal view returns (uint) nextPrng_ ) internal returns ( address[] memory ) { return ItemLib._mintRandomItems( ItemLib.MintItemInfo({ mintItems: genInfo.mintItems, mintItemsChances: genInfo.mintItemsChances, amplifier: fInfo.monsterRarity, seed: 0, oracle: IOracle(ctx.controller.oracle()), magicFind: fInfo.heroFightInfo.fighterAttributes[uint(IStatController.ATTRIBUTES.MAGIC_FIND)], destroyItems: fInfo.heroFightInfo.fighterAttributes[uint(IStatController.ATTRIBUTES.DESTROY_ITEMS)], maxItems: genInfo.maxDropItems, mintDropChanceDelta: ctx.objectSubType == uint8(IGOC.ObjectSubType.BOSS_3) ? 0 : // do not reduce drop for bosses at all StatLib.mintDropChanceDelta( fInfo.heroFightInfo.fighterStats.experience, uint8(fInfo.heroFightInfo.fighterStats.level), ctx.biome ), mintDropChanceNgLevelMultiplier: _getMintDropChanceNgLevelMultiplier(ctx) }), nextPrng_ ); } /// @return drop chance multiplier, decimals 1e18; result value is guaranteed to be <= 1e18 function _getMintDropChanceNgLevelMultiplier(IGOC.ActionContext memory ctx) internal view returns (uint) { return Math.min(1e18, RewardsPoolLib.dropChancePercent( IDungeonFactory(ctx.controller.dungeonFactory()).maxAvailableBiome(), IHeroController(ctx.controller.heroController()).maxOpenedNgLevel(), ctx.heroNgLevel )); } //endregion ------------------------ Internal calculations //region ------------------------ Utils function _context(IGOC.ActionContext memory ctx) internal view returns (AdrContext memory context) { context = AdrContext({ sender: ctx.sender, heroToken: ctx.heroToken, heroTokenId: ctx.heroTokenId, controller: ctx.controller, oracle: IOracle(ctx.controller.oracle()), statController: IStatController(ctx.controller.statController()), itemController: IItemController(ctx.controller.itemController()) }); } function unpackGeneratedMonster(bytes32 gen) internal pure returns (IGOC.GeneratedMonster memory result) { (bool generated, uint32 amplifier, int32 hp, uint8 turnCounter) = gen.unpackGeneratedMonster(); result = IGOC.GeneratedMonster({ generated: generated, amplifier: amplifier, hp: hp, turnCounter: turnCounter }); } function packGeneratedMonster(IGOC.GeneratedMonster memory gen) internal pure returns (bytes32) { return PackingLib.packGeneratedMonster(gen.generated, gen.amplifier, gen.hp, gen.turnCounter); } function packMonsterInfo(IGOC.MonsterGenInfo memory mInfo, IGOC.MonsterInfo storage info) internal { info.attributes = mInfo.attributeValues.toBytes32ArrayWithIds(mInfo.attributeIds); info.stats = PackingLib.packMonsterStats(mInfo.level, mInfo.race, mInfo.experience, mInfo.maxDropItems); info.attackInfo = PackingLib.packAttackInfo(mInfo.attackToken, mInfo.attackTokenId, mInfo.attackType); uint len = mInfo.mintItems.length; bytes32[] memory mintItems = new bytes32[](len); for (uint i; i < len; ++i) { mintItems[i] = mInfo.mintItems[i].packItemMintInfo(mInfo.mintItemsChances[i]); } info.mintItems = mintItems; } /// @return Attention: Following fields are not initialized: biome, subType, monsterId function unpackMonsterInfo(IGOC.MonsterInfo storage mInfo) internal view returns (IGOC.MonsterGenInfo memory) { IGOC.MonsterGenInfo memory result; (result.attributeValues, result.attributeIds) = mInfo.attributes.toInt32ArrayWithIds(); (result.level, result.race, result.experience, result.maxDropItems) = mInfo.stats.unpackMonsterStats(); (result.attackToken, result.attackTokenId, result.attackType) = mInfo.attackInfo.unpackAttackInfo(); uint len = mInfo.mintItems.length; result.mintItems = new address[](len); result.mintItemsChances = new uint32[](len); for (uint i = 0; i < len; i++) { (result.mintItems[i], result.mintItemsChances[i]) = mInfo.mintItems[i].unpackItemMintInfo(); } // Attention: result.biome, result.subType, result.monsterId are not initialized return result; } /// @notice Decode attack info. Ensure that attack token belongs to the hero. /// Ensure that skill tokens belong to the hero OR to the current helper (SIP-001) function decodeAndCheckAttackInfo( IItemController ic, IHeroController heroController, bytes memory data, address heroToken, uint heroId ) internal view returns (IFightCalculator.AttackInfo memory) { (IFightCalculator.AttackInfo memory attackInfo) = abi.decode(data, (IFightCalculator.AttackInfo)); if (uint(attackInfo.attackType) == 0) revert IAppErrors.UnknownAttackType(uint(attackInfo.attackType)); if (attackInfo.attackToken != address(0)) { (address h, uint hId) = ic.equippedOn(attackInfo.attackToken, attackInfo.attackTokenId); if (heroToken != h || hId != heroId) revert IAppErrors.NotYourAttackItem(); } (address helperHeroToken, uint helperHeroId) = heroController.heroReinforcementHelp(heroToken, heroId); for (uint i; i < attackInfo.skillTokens.length; ++i) { (address h, uint hId) = ic.equippedOn(attackInfo.skillTokens[i], attackInfo.skillTokenIds[i]); if ( (heroToken != h || hId != heroId) && ((helperHeroToken == address(0)) || (helperHeroToken != h || helperHeroId != hId)) ) revert IAppErrors.NotYourBuffItem(); } return attackInfo; } /// @dev Monsters power is increased on 100% with each increment of hero NG_LEVEL function monsterMultiplier(uint8 heroNgLevel) internal pure returns (uint) { return _MONSTER_MULTIPLIER_NGP_BASE * uint(heroNgLevel); } function amplifyMonsterAttribute(int32 value, uint32 amplifier, uint dungeonMultiplier) internal pure returns (int32) { if (value == 0) { return 0; } int destValue = int(value) + (int(value) * int(uint(amplifier)) / int(uint(_MAX_AMPLIFIER))) + (int(value) * int(dungeonMultiplier) / int(uint(_MAX_AMPLIFIER))); if (destValue > type(int32).max || destValue < type(int32).min) revert IAppErrors.IntValueOutOfRange(destValue); return int32(destValue); } /// @dev A wrapper around {CalcLib.pseudoRandom} to pass it as param (to be able to implement unit tests} function _pseudoRandom(uint max) internal view returns (uint) { return CalcLib.pseudoRandom(max); } /// @notice Amplify values of the attributes and of the experience /// using randomly generated {amplifier} and {dungeonMultiplier}. /// Attributes = amplify(ids, values), experience = amplify(baseExperience) function generateMonsterAttributes( uint8[] memory ids, int32[] memory values, uint32 amplifier, uint dungeonMultiplier, uint32 baseExperience ) internal pure returns ( int32[] memory attributes, uint32 experience ) { // reduce random amplifier = amplifier / 4; attributes = new int32[](uint(IStatController.ATTRIBUTES.END_SLOT)); for (uint i; i < ids.length; ++i) { attributes[ids[i]] = amplifyMonsterAttribute(values[i], amplifier, dungeonMultiplier); } experience = uint32(amplifyMonsterAttribute(int32(baseExperience), amplifier, 0)); } function _calcDmg(int32 heroLifeBefore, int32 heroLifeAfter) internal pure returns (int32 damage) { return heroLifeAfter == 0 ? heroLifeBefore : heroLifeBefore - CalcLib.minI32(heroLifeAfter, heroLifeBefore); } function _getMonsterCounterIndex(uint32 objectId) internal pure returns (bytes32) { return bytes32(abi.encodePacked("MONSTER_", StringLib._toString(uint(objectId)))); } //endregion ------------------------ Utils }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IItemController.sol"; import "../interfaces/IStatController.sol"; import "../interfaces/IAppErrors.sol"; library PackingLib { ////////////////////////// // ---- PACKING LOGIC ---- ////////////////////////// //region ------------------------------------ COMMON function packNftId(address token, uint id) internal pure returns (bytes32 serialized) { if (id > uint(type(uint64).max)) revert IAppErrors.TooHighValue(id); serialized = bytes32(uint(uint160(token))); serialized |= bytes32(uint(uint64(id))) << 160; } function unpackNftId(bytes32 data) internal pure returns (address token, uint id) { token = address(uint160(uint(data))); id = uint(data) >> 160; } function packAddressWithAmount(address token, uint amount) internal pure returns (bytes32 data) { if (amount > uint(type(uint96).max)) revert IAppErrors.TooHighValue(amount); data = bytes32(uint(uint160(token))); data |= bytes32(uint(uint96(amount))) << 160; } function unpackAddressWithAmount(bytes32 data) internal pure returns (address token, uint amount) { token = address(uint160(uint(data))); amount = uint(data) >> 160; } function packItemMintInfo(address item, uint32 chance) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(item))); data |= bytes32(uint(chance)) << 160; } function unpackItemMintInfo(bytes32 data) internal pure returns (address item, uint32 chance) { item = address(uint160(uint(data))); chance = uint32(uint(data) >> 160); } /// @param customDataIndex We assume, that two lowest bytes of this string are always zero /// So, the string looks like following: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0000 /// Last 2 bytes will be used to encode {value} function packCustomDataChange(bytes32 customDataIndex, int16 value) internal pure returns (bytes32 data) { if (uint(customDataIndex) != (uint(customDataIndex) >> 16) << 16) revert IAppErrors.IncompatibleInputString(); data = bytes32(uint(customDataIndex)); data |= bytes32(uint(uint16(value))); } function unpackCustomDataChange(bytes32 data) internal pure returns (bytes32 customDataIndex, int16 value) { customDataIndex = bytes32((uint(data) >> 16) << 16); value = int16(int(uint(uint16(uint(data))))); } /// @dev min(uint64) + max(uint64) + isHeroData/isMandatory(uint8) function packCustomDataRequirements(uint64 min, uint64 max, bool key) internal pure returns (bytes32 data) { data = bytes32(uint(min)); data |= bytes32(uint(max)) << 64; data |= bytes32(uint(key ? uint8(1) : uint8(0))) << (64 + 64); } function unpackCustomDataRequirements(bytes32 data) internal pure returns (uint64 min, uint64 max, bool key) { min = uint64(uint(data)); max = uint64(uint(data) >> 64); key = uint8(uint(data) >> (64 + 64)) == uint8(1); } function packStatsChange( uint32 experience, int32 heal, int32 manaRegen, int32 lifeChancesRecovered, int32 damage, int32 manaConsumed ) internal pure returns (bytes32 data) { data = bytes32(uint(experience)); data |= bytes32(uint(uint32(heal))) << 32; data |= bytes32(uint(uint32(manaRegen))) << (32 + 32); data |= bytes32(uint(uint32(lifeChancesRecovered))) << (32 + 32 + 32); data |= bytes32(uint(uint32(damage))) << (32 + 32 + 32 + 32); data |= bytes32(uint(uint32(manaConsumed))) << (32 + 32 + 32 + 32 + 32); } function unpackStatsChange(bytes32 data) internal pure returns ( uint32 experience, int32 heal, int32 manaRegen, int32 lifeChancesRecovered, int32 damage, int32 manaConsumed ) { experience = uint32(uint(data)); heal = int32(int(uint(data) >> 32)); manaRegen = int32(int(uint(data) >> (32 + 32))); lifeChancesRecovered = int32(int(uint(data) >> (32 + 32 + 32))); damage = int32(int(uint(data) >> (32 + 32 + 32 + 32))); manaConsumed = int32(int(uint(data) >> (32 + 32 + 32 + 32 + 32))); } function packNftIdWithValue(address token, uint id, uint32 value) internal pure returns (bytes32 serialized) { if (id > uint(type(uint64).max)) revert IAppErrors.TooHighValue(id); serialized = bytes32(uint(uint160(token))); serialized |= bytes32(uint(uint64(id))) << 160; serialized |= bytes32(uint(value)) << 160 + 64; } function unpackNftIdWithValue(bytes32 data) internal pure returns (address token, uint id, uint32 value) { token = address(uint160(uint(data))); id = uint64(uint(data) >> 160); value = uint32(uint(data) >> 160 + 64); } //endregion ------------------------------------ COMMON //region ------------------------------------ WORLD/BATTLEFIELD MAP function packMapObject(address objectAddress, uint64 objectId, uint8 objectType) internal pure returns (bytes32 packedData) { packedData = bytes32(bytes20(objectAddress)); packedData |= bytes32(uint(objectId) << 32); packedData |= bytes32(uint(objectType) << 24); } function unpackMapObject(bytes32 packedData) internal pure returns (address objectAddress, uint64 objectId, uint8 objectType) { objectAddress = address(bytes20(packedData)); objectId = uint64(uint(packedData) >> 32); objectType = uint8(uint(packedData) >> 24); } function packCoordinate(uint128 x, uint128 y) internal pure returns (bytes32 packedData) { packedData = bytes32(uint(x)); packedData |= bytes32(uint(y) << 128); } function unpackCoordinate(bytes32 packedData) internal pure returns (uint128 x, uint128 y) { x = uint128(uint(packedData)); y = uint128(uint(packedData) >> 128); } /// @param x Assume x <= max uint64 /// @param y Assume y <= max uint64 function packBattlefieldId(uint8 biomeMapFieldId, uint8 territoryNumber, uint128 x, uint128 y) internal pure returns (bytes32 packedData) { // 256 => 128 + 128; // 1) 128 is used for biomeMapFieldId, territoryNumber and probably other fields in the future // 2) 128 is used to store x, y as uint64, uint64 // we will use uint64 for coordinates assuming it is more than enough for biome map packedData = bytes32(uint(biomeMapFieldId)); packedData |= bytes32(uint(territoryNumber) << (8)); packedData |= bytes32(uint(uint64(x)) << 128); packedData |= bytes32(uint(uint64(y)) << (64 + 128)); } function unpackBattlefieldId(bytes32 packedData) internal pure returns (uint8 biomeMapFieldId, uint8 territoryNumber, uint128 x, uint128 y) { biomeMapFieldId = uint8(uint(packedData)); territoryNumber = uint8(uint(packedData) >> (8)); x = uint128(uint64(uint(packedData) >> (128))); y = uint128(uint64(uint(packedData) >> (64 + 128))); } //endregion ------------------------------------ WORLD/BATTLEFIELD MAP //region ------------------------------------ REINFORCEMENT function packReinforcementHeroInfo(uint8 biome, uint128 score, uint8 fee, uint64 stakeTs) internal pure returns (bytes32 packedData) { packedData = bytes32(uint(biome)); packedData |= bytes32(uint(score) << 8); packedData |= bytes32(uint(fee) << (8 + 128)); packedData |= bytes32(uint(stakeTs) << (8 + 128 + 8)); } function unpackReinforcementHeroInfo(bytes32 packedData) internal pure returns (uint8 biome, uint128 score, uint8 fee, uint64 stakeTs) { biome = uint8(uint(packedData)); score = uint128(uint(packedData) >> 8); fee = uint8(uint(packedData) >> (8 + 128)); stakeTs = uint64(uint(packedData) >> (8 + 128 + 8)); } function packConfigReinforcementV2(uint32 min, uint32 max, uint32 lowDivider, uint32 highDivider, uint8 levelLimit) internal pure returns (bytes32 packedData) { packedData = bytes32(uint(min)); packedData |= bytes32(uint(max) << 32); packedData |= bytes32(uint(lowDivider) << 64); packedData |= bytes32(uint(highDivider) << 96); packedData |= bytes32(uint(levelLimit) << 128); } function unpackConfigReinforcementV2(bytes32 packedData) internal pure returns (uint32 min, uint32 max, uint32 lowDivider, uint32 highDivider, uint8 levelLimit) { min = uint32(uint(packedData)); max = uint32(uint(packedData) >> 32); lowDivider = uint32(uint(packedData) >> 64); highDivider = uint32(uint(packedData) >> 96); levelLimit = uint8(uint(packedData) >> 128); } //endregion ------------------------------------ REINFORCEMENT //region ------------------------------------ DUNGEON function packDungeonKey(address heroAdr, uint80 heroId, uint16 dungLogicNum) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(heroAdr))); data |= bytes32(uint(heroId)) << 160; data |= bytes32(uint(dungLogicNum)) << (160 + 80); } function unpackDungeonKey(bytes32 data) internal pure returns (address heroAdr, uint80 heroId, uint16 dungLogicNum) { heroAdr = address(uint160(uint(data))); heroId = uint80(uint(data) >> 160); dungLogicNum = uint16(uint(data) >> (160 + 80)); } // --- GAME OBJECTS --- function packIterationKey(address heroAdr, uint64 heroId, uint32 objId) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(heroAdr))); data |= bytes32(uint(heroId)) << 160; data |= bytes32(uint(objId)) << (160 + 64); } function unpackIterationKey(bytes32 data) internal pure returns (address heroAdr, uint64 heroId, uint32 objId) { heroAdr = address(uint160(uint(data))); heroId = uint64(uint(data) >> 160); objId = uint32(uint(data) >> (160 + 64)); } function packMonsterStats( uint8 level, uint8 race, uint32 experience, uint8 maxDropItems ) internal pure returns (bytes32 data) { data = bytes32(uint(level)); data |= bytes32(uint(race)) << 8; data |= bytes32(uint(experience)) << (8 + 8); data |= bytes32(uint(maxDropItems)) << (8 + 8 + 32); } function unpackMonsterStats(bytes32 data) internal pure returns ( uint8 level, uint8 race, uint32 experience, uint8 maxDropItems ) { level = uint8(uint(data)); race = uint8(uint(data) >> 8); experience = uint32(uint(data) >> (8 + 8)); maxDropItems = uint8(uint(data) >> (8 + 8 + 32)); } function packAttackInfo( address attackToken, uint64 attackTokenId, uint8 attackType ) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(attackToken))); data |= bytes32(uint(attackTokenId)) << 160; data |= bytes32(uint(attackType)) << (160 + 64); } function unpackAttackInfo(bytes32 data) internal pure returns ( address attackToken, uint64 attackTokenId, uint8 attackType ) { attackToken = address(uint160(uint(data))); attackTokenId = uint64(uint(data) >> 160); attackType = uint8(uint(data) >> (160 + 64)); } function packPlayedObjKey(address heroAdr, uint64 heroId, uint8 oType, uint8 biome) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(heroAdr))); data |= bytes32(uint(heroId)) << 160; data |= bytes32(uint(oType)) << (160 + 64); data |= bytes32(uint(biome)) << (160 + 64 + 8); } function unpackPlayedObjKey(bytes32 data) internal pure returns (address heroAdr, uint64 heroId, uint8 oType, uint8 biome) { heroAdr = address(uint160(uint(data))); heroId = uint64(uint(data) >> 160); oType = uint8(uint(data) >> (160 + 64)); biome = uint8(uint(data) >> (160 + 64 + 8)); } function packGeneratedMonster(bool generated, uint32 amplifier, int32 hp, uint8 turnCounter) internal pure returns (bytes32 data) { data = bytes32(uint(uint8(generated ? 1 : 0))); data |= bytes32(uint(amplifier)) << 8; data |= bytes32(uint(uint32(hp))) << (8 + 32); data |= bytes32(uint(turnCounter)) << (8 + 32 + 32); } function unpackGeneratedMonster(bytes32 data) internal pure returns (bool generated, uint32 amplifier, int32 hp, uint8 turnCounter) { generated = uint8(uint(data)) == uint8(1); amplifier = uint32(uint(data) >> 8); hp = int32(int(uint(data) >> (8 + 32))); turnCounter = uint8(uint(data) >> (8 + 32 + 32)); } //endregion ------------------------------------ DUNGEON //region ------------------------------------ ITEMS /// @notice itemMetaType8 + itemLvl8 + itemType8 + baseDurability16 + defaultRarity8 + minAttr8 + maxAttr8 + manaCost32 + req(packed core 128) /// @param itemType This is ItemType enum function packItemMeta( uint8 itemMetaType, uint8 itemLvl, uint8 itemType, uint16 baseDurability, uint8 defaultRarity, uint8 minAttr, uint8 maxAttr, uint32 manaCost, IStatController.CoreAttributes memory req ) internal pure returns (bytes32 data) { data = bytes32(uint(itemMetaType)); data |= bytes32(uint(itemLvl)) << 8; data |= bytes32(uint(itemType)) << (8 + 8); data |= bytes32(uint(baseDurability)) << (8 + 8 + 8); data |= bytes32(uint(defaultRarity)) << (8 + 8 + 8 + 16); data |= bytes32(uint(minAttr)) << (8 + 8 + 8 + 16 + 8); data |= bytes32(uint(maxAttr)) << (8 + 8 + 8 + 16 + 8 + 8); data |= bytes32(uint(manaCost)) << (8 + 8 + 8 + 16 + 8 + 8 + 8); data |= bytes32(uint(int(req.strength))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32); data |= bytes32(uint(int(req.dexterity))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32); data |= bytes32(uint(int(req.vitality))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32); data |= bytes32(uint(int(req.energy))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32 + 32); } function unpackItemMeta(bytes32 data) internal pure returns (IItemController.ItemMeta memory) { IItemController.ItemMeta memory result; result.itemMetaType = uint8(uint(data)); result.itemLevel = uint8(uint(data) >> 8); result.itemType = IItemController.ItemType(uint8(uint(data) >> (8 + 8))); result.baseDurability = uint16(uint(data) >> (8 + 8 + 8)); result.defaultRarity = uint8(uint(data) >> (8 + 8 + 8 + 16)); result.minRandomAttributes = uint8(uint(data) >> (8 + 8 + 8 + 16 + 8)); result.maxRandomAttributes = uint8(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8)); result.manaCost = uint32(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8)); result.requirements.strength = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32))); result.requirements.dexterity = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32))); result.requirements.vitality = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32))); result.requirements.energy = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32 + 32))); return result; } function packItemGenerateInfo(uint8 id, int32 min, int32 max, uint32 chance) internal pure returns (bytes32 data) { data = bytes32(uint(id)); data |= bytes32(uint(uint32(min))) << 8; data |= bytes32(uint(uint32(max))) << (8 + 32); data |= bytes32(uint(chance)) << (8 + 32 + 32); } function unpackItemGenerateInfo(bytes32 data) internal pure returns (uint8 id, int32 min, int32 max, uint32 chance) { id = uint8(uint(data)); min = int32(int(uint(data) >> 8)); max = int32(int(uint(data) >> (8 + 32))); chance = uint32(uint(data) >> (8 + 32 + 32)); } function packItemAttackInfo( uint8 attackType, int32 min, int32 max, int32 factorStr, int32 factorDex, int32 factorVit, int32 factorEng ) internal pure returns (bytes32 data) { data = bytes32(uint(attackType)); data |= bytes32(uint(uint32(min))) << 8; data |= bytes32(uint(uint32(max))) << (8 + 32); data |= bytes32(uint(int(factorStr))) << (8 + 32 + 32); data |= bytes32(uint(int(factorDex))) << (8 + 32 + 32 + 32); data |= bytes32(uint(int(factorVit))) << (8 + 32 + 32 + 32 + 32); data |= bytes32(uint(int(factorEng))) << (8 + 32 + 32 + 32 + 32 + 32); } function unpackItemAttackInfo(bytes32 data) internal pure returns ( uint8 attackType, int32 min, int32 max, int32 factorStr, int32 factorDex, int32 factorVit, int32 factorEng ) { attackType = uint8(uint(data)); min = int32(int(uint(data) >> 8)); max = int32(int(uint(data) >> (8 + 32))); factorStr = int32(int(uint(data) >> (8 + 32 + 32))); factorDex = int32(int(uint(data) >> (8 + 32 + 32 + 32))); factorVit = int32(int(uint(data) >> (8 + 32 + 32 + 32 + 32))); factorEng = int32(int(uint(data) >> (8 + 32 + 32 + 32 + 32 + 32))); } function packItemInfo(uint8 rarity, uint8 augmentationLevel, uint16 durability) internal pure returns (bytes32 data) { data = bytes32(uint(rarity)); data |= bytes32(uint(augmentationLevel)) << 8; data |= bytes32(uint(durability)) << (8 + 8); } function unpackItemInfo(bytes32 data) internal pure returns (uint8 rarity, uint8 augmentationLevel, uint16 durability) { rarity = uint8(uint(data)); augmentationLevel = uint8(uint(data) >> 8); durability = uint16(uint(data) >> (8 + 8)); } function packItemBoxItemInfo(bool withdrawn, uint64 timestamp) internal pure returns (bytes32 data) { data = bytes32(uint(uint8(withdrawn ? 1 : 0))); data |= bytes32(uint(timestamp)) << 8; } function unpackItemBoxItemInfo(bytes32 data) internal pure returns (bool withdrawn, uint64 timestamp) { withdrawn = uint8(uint(data)) != 0; timestamp = uint64(uint(data) >> 8); } //endregion ------------------------------------ ITEMS //region ------------------------------------ STORIES function packStoryPageId(uint16 storyId, uint16 pageId, uint8 heroClass) internal pure returns (bytes32 data) { data = bytes32(uint(storyId)); data |= bytes32(uint(pageId)) << 16; data |= bytes32(uint(heroClass)) << (16 + 16); } function unpackStoryPageId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass) { storyId = uint16(uint(data)); pageId = uint16(uint(data) >> 16); heroClass = uint8(uint(data) >> (16 + 16)); } function packStoryAnswerId(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId) internal pure returns (bytes32 data) { data = bytes32(uint(storyId)); data |= bytes32(uint(pageId)) << 16; data |= bytes32(uint(heroClass)) << (16 + 16); data |= bytes32(uint(answerId)) << (16 + 16 + 8); } function unpackStoryAnswerId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId) { storyId = uint16(uint(data)); pageId = uint16(uint(data) >> 16); heroClass = uint8(uint(data) >> (16 + 16)); answerId = uint16(uint(data) >> (16 + 16 + 8)); } function packStoryNextPagesId(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 resultId) internal pure returns (bytes32 data) { data = bytes32(uint(storyId)); data |= bytes32(uint(pageId)) << 16; data |= bytes32(uint(heroClass)) << (16 + 16); data |= bytes32(uint(answerId)) << (16 + 16 + 8); data |= bytes32(uint(resultId)) << (16 + 16 + 8 + 16); } function unpackStoryNextPagesId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 resultId) { storyId = uint16(uint(data)); pageId = uint16(uint(data) >> 16); heroClass = uint8(uint(data) >> (16 + 16)); answerId = uint16(uint(data) >> (16 + 16 + 8)); resultId = uint8(uint(data) >> (16 + 16 + 8 + 16)); } function packStoryAttributeRequirement(uint8 attributeIndex, int32 value, bool isCore) internal pure returns (bytes32 data) { data = bytes32(uint(attributeIndex)); data |= bytes32(uint(uint32(value))) << 8; data |= bytes32(uint(isCore ? uint8(1) : uint8(0))) << (8 + 32); } function unpackStoryAttributeRequirement(bytes32 data) internal pure returns (uint8 attributeIndex, int32 value, bool isCore) { attributeIndex = uint8(uint(data)); value = int32(int(uint(data) >> 8)); isCore = uint8(uint(data) >> (8 + 32)) == uint8(1); } function packStoryItemRequirement(address item, bool requireItemBurn, bool requireItemEquipped) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(item))); data |= bytes32(uint(requireItemBurn ? uint8(1) : uint8(0))) << 160; data |= bytes32(uint(requireItemEquipped ? uint8(1) : uint8(0))) << (160 + 8); } function unpackStoryItemRequirement(bytes32 data) internal pure returns (address item, bool requireItemBurn, bool requireItemEquipped) { item = address(uint160(uint(data))); requireItemBurn = uint8(uint(data) >> 160) == uint8(1); requireItemEquipped = uint8(uint(data) >> (160 + 8)) == uint8(1); } /// @dev max amount is 309,485,009 for token with 18 decimals function packStoryTokenRequirement(address token, uint88 amount, bool requireTransfer) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(token))); data |= bytes32(uint(amount)) << 160; data |= bytes32(uint(requireTransfer ? uint8(1) : uint8(0))) << (160 + 88); } function unpackStoryTokenRequirement(bytes32 data) internal pure returns (address token, uint88 amount, bool requireTransfer) { token = address(uint160(uint(data))); amount = uint88(uint(data) >> 160); requireTransfer = uint8(uint(data) >> (160 + 88)) == uint8(1); } function packStoryCustomDataResult(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 customDataResultId) internal pure returns (bytes32 data) { data = bytes32(uint(storyId)); data |= bytes32(uint(pageId)) << 16; data |= bytes32(uint(heroClass)) << (16 + 16); data |= bytes32(uint(answerId)) << (16 + 16 + 8); data |= bytes32(uint(customDataResultId)) << (16 + 16 + 8 + 16); } function unpackStoryCustomDataResult(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 customDataResultId) { storyId = uint16(uint(data)); pageId = uint16(uint(data) >> 16); heroClass = uint8(uint(data) >> (16 + 16)); answerId = uint16(uint(data) >> (16 + 16 + 8)); customDataResultId = uint8(uint(data) >> (16 + 16 + 8 + 16)); } function packStoryHeroState(uint16 pageId, uint40 heroLastActionTS) internal pure returns (bytes32 data) { data = bytes32(uint(pageId)); data |= bytes32(uint(heroLastActionTS)) << 16; } function unpackStoryHeroState(bytes32 data) internal pure returns (uint16 pageId, uint40 heroLastActionTS) { pageId = uint16(uint(data)); heroLastActionTS = uint40(uint(data) >> 16); } function packStoryHeroStateId(address heroAdr, uint80 heroId, uint16 storyId) internal pure returns (bytes32 data) { data = bytes32(uint(uint160(heroAdr))); data |= bytes32(uint(heroId)) << 160; data |= bytes32(uint(storyId)) << (160 + 80); } function unpackStoryHeroStateId(bytes32 data) internal pure returns (address heroAdr, uint80 heroId, uint16 storyId) { heroAdr = address(uint160(uint(data))); heroId = uint80(uint(data) >> 160); storyId = uint16(uint(data) >> (160 + 80)); } function packStorySimpleRequirement(uint32 randomRequirement, uint32 delayRequirement, bool isFinalAnswer) internal pure returns (bytes32 data) { data = bytes32(uint(randomRequirement)); data |= bytes32(uint(delayRequirement)) << 32; data |= bytes32(uint(isFinalAnswer ? uint8(1) : uint8(0))) << (32 + 32); } function unpackStorySimpleRequirement(bytes32 data) internal pure returns (uint32 randomRequirement, uint32 delayRequirement, bool isFinalAnswer) { randomRequirement = uint32(uint(data)); delayRequirement = uint32(uint(data) >> 32); isFinalAnswer = uint8(uint(data) >> (32 + 32)) == uint8(1); } function packBreakInfo(uint8 slot, uint64 chance, bool stopIfBroken) internal pure returns (bytes32 data) { data = bytes32(uint(slot)); data |= bytes32(uint(chance)) << 8; data |= bytes32(uint(stopIfBroken ? uint8(1) : uint8(0))) << (8 + 64); } function unpackBreakInfo(bytes32 data) internal pure returns (uint8 slot, uint64 chance, bool stopIfBurned) { slot = uint8(uint(data)); chance = uint64(uint(data) >> 8); stopIfBurned = uint8(uint(data) >> (8 + 64)) == uint8(1); } //endregion ------------------------------------ STORIES //region ------------------------------------ Hero controller function packTierHero(uint8 tier, address hero) internal pure returns (bytes32 packedTierHero) { packedTierHero = bytes32(uint(tier)); packedTierHero |= bytes32(uint(uint160(hero)) << 8); } function unpackTierHero(bytes32 packedTierHero) internal pure returns (uint8 tier, address hero) { tier = uint8(uint(packedTierHero)); hero = address(uint160(uint(packedTierHero) >> 8)); } //endregion ------------------------------------ Hero controller //////////////////////////////////////////////////////////////////////////////////// // ---- ARRAYS LOGIC ---- //////////////////////////////////////////////////////////////////////////////////// //region ------------------------------------ SIMPLE ARRAYS function packUint8Array(uint8[] memory data) internal pure returns (bytes32) { uint len = data.length; if (len > 32) revert IAppErrors.OutOfBounds(len, 32); bytes32 result; for (uint i = 0; i < len; i++) { result |= bytes32(uint(data[i])) << (i * 8); } return result; } /// @notice Simple faster version of {packUint8Array} for small number of items /// It allows to exclude dynamic array creation. function packUint8Array3(uint8 a, uint8 b, uint8 c) internal pure returns (bytes32) { bytes32 result = bytes32(uint(a)); result |= bytes32(uint(b)) << (1 * 8); result |= bytes32(uint(c)) << (2 * 8); return result; } function unpackUint8Array(bytes32 data) internal pure returns (uint8[] memory) { uint8[] memory result = new uint8[](32); for (uint i = 0; i < 32; i++) { result[i] = uint8(uint(data) >> (i * 8)); } return result; } /// @notice Simple faster version of {unpackUint8Array} for small number of items /// It allows to exclude only first 3 values function unpackUint8Array3(bytes32 data) internal pure returns (uint8 a, uint8 b, uint8 c) { a = uint8(uint(data)); b = uint8(uint(data) >> (1 * 8)); c = uint8(uint(data) >> (2 * 8)); } function changeUnit8ArrayWithCheck(bytes32 data, uint index, uint8 value, uint8 expectedPrevValue) internal pure returns (bytes32 newData) { uint8[] memory arr = unpackUint8Array(data); if (arr[index] != expectedPrevValue) revert IAppErrors.UnexpectedValue(uint(expectedPrevValue), uint(arr[index])); arr[index] = value; return packUint8Array(arr); } function packInt32Array(int32[] memory data) internal pure returns (bytes32) { uint len = data.length; if (len > 8) revert IAppErrors.OutOfBounds(len, 8); bytes32 result; for (uint i; i < len; i++) { result |= bytes32(uint(uint32(data[i]))) << (i * 32); } return result; } function unpackInt32Array(bytes32 data) internal pure returns (int32[] memory) { int32[] memory result = new int32[](8); for (uint i = 0; i < 8; i++) { result[i] = int32(int(uint(data) >> (i * 32))); } return result; } function packUint32Array(uint32[] memory data) internal pure returns (bytes32) { uint len = data.length; if (len > 8) revert IAppErrors.OutOfBounds(len, 8); bytes32 result; for (uint i = 0; i < len; i++) { result |= bytes32(uint(data[i])) << (i * 32); } return result; } function unpackUint32Array(bytes32 data) internal pure returns (uint32[] memory) { uint32[] memory result = new uint32[](8); for (uint i = 0; i < 8; i++) { result[i] = uint32(uint(data) >> (i * 32)); } return result; } //endregion ------------------------------------ SIMPLE ARRAYS //region ------------------------------------ COMPLEX ARRAYS // We should represent arrays without concrete size. // For this reason we must not revert IAppErrors.on out of bounds but return zero value instead. // we need it for properly unpack packed arrays with ids // function getInt32AsInt24(bytes32[] memory arr, uint idx) internal pure returns (int32) { // if (idx / 8 >= arr.length) { // return int32(0); // } // return int32(int24(int(uint(arr[idx / 8]) >> ((idx % 8) * 32)))); // } // we need it for properly unpack packed arrays with ids // function getUnit8From32Step(bytes32[] memory arr, uint idx) internal pure returns (uint8) { // if (idx / 8 >= arr.length) { // return uint8(0); // } // return uint8(uint(arr[idx / 8]) >> ((idx % 8) * 32 + 24)); // } function getInt32Memory(bytes32[] memory arr, uint idx) internal pure returns (int32) { if (idx / 8 >= arr.length) { return int32(0); } return int32(int(uint(arr[idx / 8]) >> ((idx % 8) * 32))); } function getInt32(bytes32[] storage arr, uint idx) internal view returns (int32) { // additional gas usage, but we should not revert IAppErrors.on out of bounds if (idx / 8 >= arr.length) { return int32(0); } return int32(int(uint(arr[idx / 8]) >> ((idx % 8) * 32))); } function setInt32(bytes32[] storage arr, uint idx, int32 value) internal { uint pos = idx / 8; uint shift = (idx % 8) * 32; uint curLength = arr.length; if (pos >= curLength) { arr.push(0); for (uint i = curLength; i < pos; ++i) { arr.push(0); } } arr[pos] = bytes32(uint(arr[pos]) & ~(uint(0xffffffff) << shift) | (uint(uint32(value)) & 0xffffffff) << shift); } /// @notice Increment {idx}-th item on {value} function changeInt32(bytes32[] storage arr, uint idx, int32 value) internal returns (int32 newValue, int32 change) { int32 cur = int32(int(getInt32(arr, idx))); int newValueI = int(cur) + int(value); newValue = int32(newValueI); change = int32(newValueI - int(cur)); setInt32(arr, idx, newValue); } function toInt32Array(bytes32[] memory arr, uint size) internal pure returns (int32[] memory) { int32[] memory result = new int32[](size); for (uint i = 0; i < arr.length; i++) { for (uint j; j < 8; ++j) { uint idx = i * 8 + j; if (idx >= size) break; result[idx] = getInt32Memory(arr, idx); } } return result; } /// @dev pack int32 array into bytes32 array function toBytes32Array(int32[] memory arr) internal pure returns (bytes32[] memory) { uint size = arr.length / 8 + 1; bytes32[] memory result = new bytes32[](size); for (uint i; i < size; ++i) { for (uint j; j < 8; ++j) { uint idx = i * 8 + j; if (idx >= arr.length) break; result[i] |= bytes32(uint(uint32(arr[idx]))) << (j * 32); } } return result; } /// @dev pack int32 array into bytes32 array using last 8bytes for ids /// we can not use zero values coz will not able to properly unpack it later function toBytes32ArrayWithIds(int32[] memory arr, uint8[] memory ids) internal pure returns (bytes32[] memory) { if (arr.length != ids.length) revert IAppErrors.LengthsMismatch(); uint size = arr.length / 8 + 1; bytes32[] memory result = new bytes32[](size); for (uint i; i < size; ++i) { for (uint j; j < 8; ++j) { uint idx = i * 8 + j; if (idx >= arr.length) break; if (arr[idx] > type(int24).max || arr[idx] < type(int24).min) revert IAppErrors.IntOutOfRange(int(arr[idx])); if (arr[idx] == 0) revert IAppErrors.ZeroValue(); result[i] |= bytes32(uint(uint24(int24(arr[idx])))) << (j * 32); result[i] |= bytes32(uint(ids[idx])) << (j * 32 + 24); } } return result; } /// @dev we do not know exact size of array, assume zero values is not acceptable for this array function toInt32ArrayWithIds(bytes32[] memory arr) internal pure returns (int32[] memory values, uint8[] memory ids) { uint len = arr.length; uint size = len * 8; int32[] memory valuesTmp = new int32[](size); uint8[] memory idsTmp = new uint8[](size); uint counter; for (uint i = 0; i < len; i++) { for (uint j; j < 8; ++j) { uint idx = i * 8 + j; // if (idx >= size) break; // it looks like a useless check valuesTmp[idx] = int32(int24(int(uint(arr[i]) >> (j * 32)))); // getInt32AsInt24(arr, idx); idsTmp[idx] = uint8(uint(arr[i]) >> (j * 32 + 24)); // getUnit8From32Step(arr, idx); if (valuesTmp[idx] == 0) { break; } counter++; } } values = new int32[](counter); ids = new uint8[](counter); for (uint i; i < counter; ++i) { values[i] = valuesTmp[i]; ids[i] = idsTmp[i]; } } //endregion ------------------------------------ COMPLEX ARRAYS //region ------------------------------------ Guilds /// @dev ShelterID is uint. But in the code we assume that this ID can be stored as uint64 (see auctions) /// @param biome 1, 2, 3... /// @param shelterLevel 1, 2 or 3. /// @param shelterIndex 0, 1, 2 ... function packShelterId(uint8 biome, uint8 shelterLevel, uint8 shelterIndex) internal pure returns (uint) { return uint(biome) | (uint(shelterLevel) << 8) | (uint(shelterIndex) << 16); } function unpackShelterId(uint shelterId) internal pure returns (uint8 biome, uint8 shelterLevel, uint8 shelterIndex) { return (uint8(shelterId), uint8(shelterId >> 8), uint8(shelterId >> 16)); } //endregion ------------------------------------ Guilds //region ------------------------------------ Metadata of IItemController.OtherSubtypeKind function getOtherItemTypeKind(bytes memory packedData) internal pure returns (IItemController.OtherSubtypeKind) { bytes32 serialized; assembly { serialized := mload(add(packedData, 32)) } uint8 kind = uint8(uint(serialized)); if (kind == 0 || kind >= uint8(IItemController.OtherSubtypeKind.END_SLOT)) revert IAppErrors.IncorrectOtherItemTypeKind(kind); return IItemController.OtherSubtypeKind(kind); } function packOtherItemReduceFragility(uint value) internal pure returns (bytes memory packedData) { bytes32 serialized = bytes32(uint(uint8(IItemController.OtherSubtypeKind.REDUCE_FRAGILITY_1))); serialized |= bytes32(uint(uint248(value))) << 8; return bytes.concat(serialized); } function unpackOtherItemReduceFragility(bytes memory packedData) internal pure returns (uint) { bytes32 serialized; assembly { serialized := mload(add(packedData, 32)) } uint8 kind = uint8(uint(serialized)); if (kind != uint8(IItemController.OtherSubtypeKind.REDUCE_FRAGILITY_1)) revert IAppErrors.IncorrectOtherItemTypeKind(kind); uint value = uint248(uint(serialized) >> 8); return value; } //endregion ------------------------------------ Metadata of IItemController.OtherSubtypeKind //region ------------------------------------ Metadata of IPvpController.PvpAttackInfoDefaultStrategy function getPvpBehaviourStrategyKind(bytes memory encodedData) internal pure returns (uint) { bytes32 serialized; assembly { serialized := mload(add(encodedData, 64)) // first 32 bytes contain 0x20 and indicate array, we need to read second 32 bytes to get first uint in the struct } return uint(serialized); } //endregion ------------------------------------ Metadata of IPvpController.PvpAttackInfoDefaultStrategy }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IERC721.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IPvpController.sol"; import "../interfaces/IReinforcementController.sol"; import "../interfaces/IGuildStakingAdapter.sol"; import "./CalcLib.sol"; import "./ControllerContextLib.sol"; import "./ScoreLib.sol"; import "./MonsterLib.sol"; import "./AppLib.sol"; import "./PackingLib.sol"; import "./PvpControllerLib.sol"; import "./ReinforcementControllerLib.sol"; library PvpAttackLib { //region ------------------------ Constants /// @notice Max total count of turns per pvp-fight uint internal constant MAX_COUNT_TURNS = 100; //endregion ------------------------ Constants //region ------------------------ Data types struct PvpFightContext { uint8 turn; IFightCalculator.AttackInfo heroAttackInfo; IFightCalculator.AttackInfo opponentAttackInfo; int32 heroBuffManaConsumed; int32 opponentBuffManaConsumed; } struct PvpFightParams { /// @notice Max count of fights between the heroes uint8 maxCountTurns; /// @notice Count of fights made (max count is limited by 100) uint8 countTurnsMade; uint48 fightId; /// @notice User that starts the fight address msgSender; IItemController itemController; IStatController statController; IStatController.ChangeableStats heroStats; IStatController.ChangeableStats opponentStats; } //endregion ------------------------ Data types //endregion ------------------------ Action /// @notice Start or continue fighting between the hero and his opponent. /// A series of fights takes place until the health of one heroes drops to zero. /// If the total number of fights reaches 100 and none of the heroes' health reaches zero, /// the winner is a hero with the most lives. If the numbers of lives are equal, the winner is selected randomly. /// @dev Assume here that both hero tokens are not burnt function pvpFight( PvpFightParams memory p, IPvpController.HeroData memory hero, IPvpController.HeroData memory opponent ) external returns ( IPvpController.PvpFightResults memory fightResult ) { return _pvpFight(p, hero, opponent, CalcLib.pseudoRandom, FightLib.fight); } /// @param random_ Pass _pseudoRandom here, param is required to simplify unit testing /// @param fight_ Pass FightLib.fight here, param is required to simplify unit testing function _pvpFight( PvpFightParams memory p, IPvpController.HeroData memory hero, IPvpController.HeroData memory opponent, function (uint) internal view returns (uint) random_, function( IItemController, IFightCalculator.FightCall memory, IFightCalculator.FightCallAdd memory, function (uint) internal view returns (uint) ) internal returns (IFightCalculator.FightResult memory) fight_ ) internal returns ( IPvpController.PvpFightResults memory fightResult ) { PvpFightContext memory v; // check attackInfo and remove all unusable tokens from there // these attackInfo will be used as a base to generate attackInfo on each turn v.heroAttackInfo = _prepareAttackInfo(p.itemController, hero); v.opponentAttackInfo = _prepareAttackInfo(p.itemController, opponent); // fight until one of the heroes dies OR max count of turns is reached fightResult = IPvpController.PvpFightResults({ completed: p.heroStats.life == 0 || p.opponentStats.life == 0, totalCountFights: p.countTurnsMade, healthHero: p.heroStats.life, healthOpponent: p.opponentStats.life, manaConsumedHero: 0, manaConsumedOpponent: 0 }); if (!fightResult.completed) { for (uint8 i; i < p.maxCountTurns; ++i) { IFightCalculator.FighterInfo memory heroFightInfo; (heroFightInfo, v.heroBuffManaConsumed) = _generateHeroFightInfo( p.statController, p.itemController, p.heroStats, v.heroAttackInfo, hero, fightResult.healthHero, fightResult.manaConsumedHero ); IFightCalculator.FighterInfo memory opponentFightInfo; (opponentFightInfo, v.opponentBuffManaConsumed) = _generateHeroFightInfo( p.statController, p.itemController, p.opponentStats, v.opponentAttackInfo, opponent, fightResult.healthOpponent, fightResult.manaConsumedOpponent ); MonsterLib._debuff(opponentFightInfo.fighterAttributes, v.heroAttackInfo, p.itemController); MonsterLib._debuff(heroFightInfo.fighterAttributes, v.opponentAttackInfo, p.itemController); // take into account all mana consumed by buff fightResult.manaConsumedHero = _subMana(fightResult.manaConsumedHero, -v.heroBuffManaConsumed); fightResult.manaConsumedOpponent = _subMana(fightResult.manaConsumedOpponent, -v.opponentBuffManaConsumed); v.turn = fightResult.totalCountFights; // abs index of the fight, it's important for UI IFightCalculator.FightResult memory result = fight_( p.itemController, IFightCalculator.FightCall({ fighterA: heroFightInfo, fighterB: opponentFightInfo, dungeonId: 0, objectId: 0, heroAdr: hero.hero, heroId: hero.heroId, stageId: 0, iteration: 0, turn: v.turn }), IFightCalculator.FightCallAdd({ msgSender: p.msgSender, fightId: p.fightId }), random_ ); // assume that fight_ emits PvpFightResultProcessed with all detailed info for the current turn of the fight // so there is no other event here fightResult.healthHero = uint32(result.healthA); fightResult.healthOpponent = uint32(result.healthB); fightResult.manaConsumedHero += uint32(result.manaConsumedA); fightResult.manaConsumedOpponent += uint32(result.manaConsumedB); fightResult.totalCountFights++; if (fightResult.healthHero == 0 || fightResult.healthOpponent == 0 || fightResult.totalCountFights >= MAX_COUNT_TURNS) { fightResult.completed = true; break; } } } return fightResult; } //endregion ------------------------ Action //region ------------------------ Internal logic - prepare attack info /// @notice Check {hero.attackInfo} passed by the hero's user and exclude any not-valid tokens /// Selection of magic attack/skill on this stage means that they SHOULD BE used but ONLY IF it's possible. function _prepareAttackInfo(IItemController ic, IPvpController.HeroData memory hero) internal view returns ( IFightCalculator.AttackInfo memory dest ) { // assume here that hero hero.pvpStrategy has kind DEFAULT_STRATEGY_0 (we check it on staking) IPvpController.PvpStrategyDefault memory decoded = abi.decode(hero.pvpStrategy, (IPvpController.PvpStrategyDefault)); dest.attackType = IFightCalculator.AttackType.MELEE; if (decoded.attackInfo.attackType == IFightCalculator.AttackType.MAGIC) { if (decoded.attackInfo.attackToken != address(0)) { (address h, uint hId) = ic.equippedOn(decoded.attackInfo.attackToken, decoded.attackInfo.attackTokenId); if (hero.hero == h || hId == hero.heroId) { dest.attackType = IFightCalculator.AttackType.MAGIC; dest.attackToken = decoded.attackInfo.attackToken; dest.attackTokenId = decoded.attackInfo.attackTokenId; } } } // keep only actually equipped skills uint len = decoded.attackInfo.skillTokens.length; if (len != 0) { uint countValidTokens = 0; uint[] memory indicesValidTokens = new uint[](len); for (uint i; i < len; ++i) { (address h, uint hId) = ic.equippedOn(decoded.attackInfo.skillTokens[i], decoded.attackInfo.skillTokenIds[i]); if ((hero.hero == h || hId == hero.heroId)) { indicesValidTokens[countValidTokens++] = i; } } dest.skillTokenIds = new uint[](countValidTokens); dest.skillTokens = new address[](countValidTokens); for (uint i; i < countValidTokens; ++i) { dest.skillTokens[i] = decoded.attackInfo.skillTokens[indicesValidTokens[i]]; dest.skillTokenIds[i] = decoded.attackInfo.skillTokenIds[indicesValidTokens[i]]; } } return dest; } function _generateHeroFightInfo( IStatController statController, IItemController itemController, IStatController.ChangeableStats memory heroStats, IFightCalculator.AttackInfo memory heroAttackInfo, IPvpController.HeroData memory hero, uint32 healthHero, uint32 manaConsumedHero ) internal view returns ( IFightCalculator.FighterInfo memory, int32 manaConsumed ) { // use all available skills to buff int32[] memory heroAttributes; (heroAttributes, manaConsumed) = MonsterLib._buffAndGetHeroAttributes( heroStats.level, heroAttackInfo.skillTokens, heroAttackInfo.skillTokenIds, statController, hero.hero, hero.heroId ); uint32 newMana = _subMana(heroStats.mana, int32(int(uint(manaConsumedHero))) + manaConsumed); // generate attack info IFightCalculator.AttackInfo memory attackInfo; attackInfo.attackType = IFightCalculator.AttackType.MELEE; if (newMana != 0) { if (heroAttackInfo.attackType == IFightCalculator.AttackType.MAGIC) { uint32 manaCost = itemController.itemMeta(heroAttackInfo.attackToken).manaCost; if (newMana >= manaCost) { attackInfo.attackType = IFightCalculator.AttackType.MAGIC; attackInfo.attackToken = heroAttackInfo.attackToken; attackInfo.attackTokenId = heroAttackInfo.attackTokenId; newMana -= manaCost; manaConsumed += int32(int(uint(manaCost))); } } } IFightCalculator.FighterInfo memory fi = IFightCalculator.FighterInfo({ fighterAttributes: heroAttributes, // take into account health and mana already lost in the current fight fighterStats: IStatController.ChangeableStats({ level: heroStats.level, lifeChances: heroStats.lifeChances, experience: heroStats.experience, mana: newMana, life: healthHero }), attackType: attackInfo.attackType, attackToken: attackInfo.attackToken, attackTokenId: attackInfo.attackTokenId, race: uint(IStatController.Race.HUMAN) }); return (fi, manaConsumed); } function _subMana(uint32 mana, int32 consumedMana) internal pure returns (uint32) { return consumedMana < 0 ? mana + uint32(-consumedMana) : AppLib.sub0(mana, uint32(consumedMana)); } //endregion ------------------------ Internal logic - prepare attack info }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IERC721.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IPvpController.sol"; import "../interfaces/IReinforcementController.sol"; import "../interfaces/IGuildStakingAdapter.sol"; import "./CalcLib.sol"; import "./ControllerContextLib.sol"; import "./ScoreLib.sol"; import "./MonsterLib.sol"; import "./AppLib.sol"; import "./PackingLib.sol"; import "./ReinforcementControllerLib.sol"; library PvpControllerLib { using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMap for EnumerableMap.AddressToUintMap; using EnumerableMap for EnumerableMap.UintToUintMap; //region ------------------------ Constants /// @dev keccak256(abi.encode(uint256(keccak256("pvp.controller.main")) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant PVP_CONTROLLER_STORAGE_LOCATION = 0x6750db0cf5db3c73c8abbeff54ef9c65daabbed6967cb68f37e0698f5fc7bb00; /// @notice First guild level starting from which the pvp-fights are allowed uint internal constant MIN_GUILD_LEVEL_REQUIRED_FOR_PVP = 3; /// @notice Fight-winner is allowed to make more pvp-fights in the same epoch bool internal constant MULTI_FIGHTS_PER_EPOCH_ALLOWED_FOR_WINNERS = true; /// @notice Hero can be pvp-staked if his level is greater of equal to the given min level uint32 internal constant DEFAULT_MIN_HERO_LEVEL = 5; /// @notice Max number of heroes that any user can pvp-stakes per single epoch /// @dev uint32 is used to be able to store max value inside UserState, -1 is for unit tests uint32 internal constant MAX_NUMBER_STAKES_FOR_USER_PER_EPOCH = type(uint32).max - 1; //endregion ------------------------ Constants //region ------------------------ Data types struct AddPvpHeroLocal { uint8 targetBiome; uint32 week; bytes32 packedHero; IGuildController guildController; IHeroController heroController; IStatController statController; address opponent; bytes32 opponentPackedHero; uint opponentGuildId; uint guildId; IPvpController.PvpUserState userState; SetupPvpFightParams eventParams; } struct SetupPvpFightParams { uint32 week; address hero; uint heroId; bytes32 opponentPackedHero; } //endregion ------------------------ Data types //region ------------------------ Storage function _S() internal pure returns (IPvpController.MainState storage s) { assembly { s.slot := PVP_CONTROLLER_STORAGE_LOCATION } return s; } //endregion ------------------------ Storage //region ------------------------ Restrictions function _onlyNotPaused(IController controller) internal view { if (controller.onPause()) revert IAppErrors.ErrorPaused(); } function _onlyGuildController(IController controller) internal view { if (controller.guildController() != msg.sender) revert IAppErrors.ErrorNotGuildController(); } function _onlyDeployer(IController controller) internal view { if (!controller.isDeployer(msg.sender)) revert IAppErrors.ErrorNotDeployer(msg.sender); } function _onlyUserWithRegisteredPvpHeroWithoutFights(IPvpController.PvpUserState memory userState) internal pure { if (userState.biome == 0) revert IAppErrors.PvpHeroNotRegistered(); if (userState.activeFightIndex1 != 0) revert IAppErrors.PvpHeroHasInitializedFight(); } //endregion ------------------------ Restrictions //region ------------------------ View function getBiomeOwner(uint8 biome) internal view returns (uint guildId) { return _S().biomeState[biome].guildBiomeOwnerId; } function getStartedEpoch(uint8 biome) internal view returns (uint32 epochWeek) { return _S().biomeState[biome].startedEpochWeek; } function getDominationCounter(uint8 biome) internal view returns (uint16 dominationCounter) { return _S().biomeState[biome].dominationCounter; } /// @notice List of guilds that send domination request for the biome function getBiomeGuilds(uint8 biome, uint32 epochWeek) internal view returns (uint[] memory guildIds) { return _S().epochData[epochWeek].biomeGuilds[biome].values(); } /// @return biome Biome where the guild is going to dominate in the given epoch function getDominationRequest(uint guildId, uint32 epochWeek) internal view returns (uint8 biome) { return _S().epochData[epochWeek].targetBiome[guildId]; } function getGuildPoints(uint8 biome, uint32 epochWeek, uint guildId) internal view returns (uint) { (bool exist, uint countPoints) = _S().epochData[epochWeek].epochBiomeData[biome].guildPoints.tryGet(guildId); return exist ? countPoints : 0; } function getFreeUsers(uint8 biome, uint32 epochWeek, uint guildId) internal view returns (address[] memory) { return _S().epochData[epochWeek].epochBiomeData[biome].freeUsers[guildId].values(); } function getPvpStrategy(uint8 biome, uint32 epochWeek, address hero, uint heroId) internal view returns (bytes memory) { return _S().epochData[epochWeek].epochBiomeData[biome].pvpStrategy[PackingLib.packNftId(hero, heroId)]; } function getPvpStrategyKind(uint8 biome, uint32 epochWeek, address hero, uint heroId) internal view returns (uint) { return PackingLib.getPvpBehaviourStrategyKind(_S().epochData[epochWeek].epochBiomeData[biome].pvpStrategy[PackingLib.packNftId(hero, heroId)]); } function getFightDataLength(uint32 epochWeek, address user) internal view returns (uint) { return _S().epochData[epochWeek].fightData[user].length; } function getFightDataByIndex(uint32 epochWeek, address user, uint index0) internal view returns (IPvpController.PvpFightData memory) { return _S().epochData[epochWeek].fightData[user][index0]; } function registeredUsers(uint8 biome, uint32 epochWeek, uint guildId) internal view returns (address[] memory) { return _S().epochData[epochWeek].epochBiomeData[biome].registeredHeroes[guildId].keys(); } function registeredHero(uint8 biome, uint32 epochWeek, uint guildId, address user) internal view returns (address hero, uint heroId) { (bool exist, uint packedHero) = _S().epochData[epochWeek].epochBiomeData[biome].registeredHeroes[guildId].tryGet(user); if (exist) { (hero, heroId) = PackingLib.unpackNftId(bytes32(packedHero)); } return (hero, heroId); } function ownedBiome(uint guildId) internal view returns (uint8 biome) { return _S().ownedBiome[guildId]; } /// @notice Get biome tax /// @return guildId Owner of the biome /// @return taxPercent Final tax percent, [0...100_000], decimals 3 function getBiomeTax(uint8 biome) internal view returns (uint guildId, uint taxPercent) { return _getBiomeTax(_S().biomeState[biome]); } /// @notice Check if the user has a pvp-hero registered for pvp-fight in the given epoch function hasPvpHero(address user, uint guildId, uint32 week) internal view returns (bool) { IPvpController.EpochData storage epochData = _S().epochData[week]; uint8 biome = epochData.targetBiome[guildId]; return biome == 0 ? false : epochData.epochBiomeData[biome].registeredHeroes[guildId].contains(user); } /// @notice Check if the given hero is staked in pvp controller in the given epoch function isHeroStaked(address hero, uint heroId, uint32 epochWeek) internal view returns (bool staked) { IPvpController.EpochData storage epochData = _S().epochData[epochWeek]; return epochData.stakedHeroes.contains(uint(PackingLib.packNftId(hero, heroId))); } function getUserState(uint32 week, address user) internal view returns (IPvpController.PvpUserState memory) { return _S().epochData[week].pvpUserState[user]; } function getMinHeroLevel() internal view returns (uint) { return _S().pvpParam[IPvpController.PvpParams.MIN_HERO_LEVEL_1]; } function getCounterFightId() internal view returns (uint48) { return uint48(_S().pvpParam[IPvpController.PvpParams.FIGHT_COUNTER_3]); } //endregion ------------------------ View //region ------------------------ Deployer actions function setMinHeroLevel(IController controller, uint level) internal { _onlyDeployer(controller); _S().pvpParam[IPvpController.PvpParams.MIN_HERO_LEVEL_1] = level; emit IApplicationEvents.SetMinHeroLevel(level); } function setGuildStakingAdapter(IController controller, address adapter_) internal { _onlyDeployer(controller); _S().pvpParam[IPvpController.PvpParams.GUILD_STAKING_ADAPTER_2] = uint(uint160(adapter_)); emit IApplicationEvents.SetGuildStakingAdapter(adapter_); } //endregion ------------------------ Deployer actions //region ------------------------ Domination actions /// @notice Create new request for domination. New request can be created once per epoch /// @param biome Biome selected by the guild for domination in the current epoch /// @param random_ CalcLib.pseudoRandom, required for unit tests function selectBiomeForDomination( address msgSender, IController controller, uint8 biome, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal { _onlyNotPaused(controller); IGuildController guildController = IGuildController(controller.guildController()); if (biome == 0) revert IAppErrors.ErrorIncorrectBiome(biome); (uint guildId,) = _checkPermissions(guildController, msgSender, IGuildController.GuildRightBits.DOMINATION_REQUEST_13); { (,,, uint8 guildLevel,,) = guildController.getGuildData(guildId); if (guildLevel < MIN_GUILD_LEVEL_REQUIRED_FOR_PVP) revert IAppErrors.TooLowGuildLevel(); } uint32 week = getCurrentEpochWeek(blockTimestamp); IPvpController.EpochData storage epochData = _S().epochData[week]; if (epochData.targetBiome[guildId] != 0) revert IAppErrors.BiomeAlreadySelected(); _updateEpoch(biome, blockTimestamp, random_); // register new domination request epochData.targetBiome[guildId] = biome; epochData.biomeGuilds[biome].add(guildId); emit IApplicationEvents.AddBiomeRequest(msgSender, biome, guildId, week); } /// @notice Register hero for pvp. User is able to register only one hero at any moment. /// @param random_ CalcLib.pseudoRandom, required for unit tests function addPvpHero( address msgSender, IController controller, address hero, uint heroId, bytes memory pvpStrategyData, uint8 maxFights, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal { _onlyNotPaused(controller); AddPvpHeroLocal memory v; v.guildController = IGuildController(controller.guildController()); v.heroController = IHeroController(controller.heroController()); v.statController = IStatController(controller.statController()); v.week = getCurrentEpochWeek(blockTimestamp); // any guild member can participate in pvp, no permissions are required (v.guildId, v.targetBiome) = _getTargetDominationBiomeWithCheck(msgSender, v.guildController, v.week, true); if (IERC721(hero).ownerOf(heroId) != msgSender) revert IAppErrors.ErrorNotOwner(hero, heroId); if (v.heroController.heroClass(hero) == 0) revert IAppErrors.ErrorHeroIsNotRegistered(hero); if (IReinforcementController(controller.reinforcementController()).isStaked(hero, heroId)) revert IAppErrors.Staked(hero, heroId); if (!v.statController.isHeroAlive(hero, heroId)) revert IAppErrors.ErrorHeroIsDead(hero, heroId); if (v.heroController.sandboxMode(hero, heroId) == uint8(IHeroController.SandboxMode.SANDBOX_MODE_1)) revert IAppErrors.SandboxModeNotAllowed(); if (IDungeonFactory(controller.dungeonFactory()).currentDungeon(hero, heroId) != 0) revert IAppErrors.HeroInDungeon(); if (v.heroController.countHeroTransfers(hero, heroId) > 1) revert IAppErrors.HeroWasTransferredBetweenAccounts(); // assume here that there is no reason to check guild level - it's high enough as soon as targetBiome != 0 { uint32 heroLevel = v.statController.heroStats(hero, heroId).level; if (heroLevel < getMinHeroLevel()) revert IAppErrors.ErrorLevelTooLow(heroLevel); } _updateEpoch(v.targetBiome, blockTimestamp, random_); // check current fight status IPvpController.EpochData storage epochData = _S().epochData[v.week]; v.userState = epochData.pvpUserState[msgSender]; if (v.userState.biome != 0) revert IAppErrors.UserHasRegisteredPvpHeroInBiome(v.userState.biome); if (v.userState.numHeroesStaked >= MAX_NUMBER_STAKES_FOR_USER_PER_EPOCH) revert IAppErrors.UserNotAllowedForPvpInCurrentEpoch(v.week); // register new hero IPvpController.EpochBiomeData storage epochBiomeData = epochData.epochBiomeData[v.targetBiome]; v.packedHero = PackingLib.packNftId(hero, heroId); { // attackInfo params are NOT validated here, they will be checked just before using uint pvpStrategyKind = PackingLib.getPvpBehaviourStrategyKind(pvpStrategyData); if (pvpStrategyKind != uint(IPvpController.PvpBehaviourStrategyKinds.DEFAULT_STRATEGY_0)) revert IAppErrors.UnknownPvpStrategy(); epochBiomeData.pvpStrategy[v.packedHero] = pvpStrategyData; } epochBiomeData.registeredHeroes[v.guildId].set(msgSender, uint(v.packedHero)); epochData.stakedHeroes.add(uint(v.packedHero)); // initialize new user state epochData.pvpUserState[msgSender] = IPvpController.PvpUserState({ biome: v.targetBiome, guildId: uint64(v.guildId), activeFightIndex1: 0, // there is no active fight at this moment numHeroesStaked: 1 + v.userState.numHeroesStaked, countFights: 0, maxFights: maxFights, fightId: 0 // there is no active fight at this moment }); // emit PvpHeroAdded before emitting of PreparePvpFight emit IApplicationEvents.PvpHeroAdded(msgSender, v.guildId, hero, heroId, v.week, v.targetBiome); // try to find opponent for the newly registered hero and initialize the fight if an opponent is found (v.opponent, v.opponentPackedHero, v.opponentGuildId) = _findPvpOpponent(v.guildController, v.targetBiome, epochData, v.guildId, random_); if (v.opponent == address(0)) { epochBiomeData.freeUsers[v.guildId].add(msgSender); } else { v.eventParams = SetupPvpFightParams({ week: v.week, hero: hero, heroId: heroId, opponentPackedHero: v.opponentPackedHero }); _setupPvpFight(v.targetBiome, epochData, msgSender, v.opponent, v.guildId, v.opponentGuildId, v.eventParams); } } /// @notice Remove pvp-hero registered by the {msgSender}. /// It's allowed only if pvp-hero has no initialized fight. function removePvpHero(address msgSender, IController controller, uint blockTimestamp) internal { _onlyNotPaused(controller); uint32 week = getCurrentEpochWeek(blockTimestamp); IPvpController.EpochData storage epochData = _S().epochData[week]; IPvpController.PvpUserState memory userState = epochData.pvpUserState[msgSender]; _onlyUserWithRegisteredPvpHeroWithoutFights(userState); IPvpController.EpochBiomeData storage epochBiomeData = epochData.epochBiomeData[userState.biome]; _removePvpHero(week, epochData, epochBiomeData, userState, msgSender, true); } /// @param manualRemoving True if the hero is remove manually by the user, false - he is removed automatically after the fight function _removePvpHero( uint32 week, IPvpController.EpochData storage epochData, IPvpController.EpochBiomeData storage epochBiomeData, IPvpController.PvpUserState memory userState, address user, bool manualRemoving ) internal { address hero; uint heroId; (bool exist, uint packedHeroAsUint) = epochBiomeData.registeredHeroes[userState.guildId].tryGet(user); if (exist) { epochBiomeData.registeredHeroes[userState.guildId].remove(user); epochData.stakedHeroes.remove(packedHeroAsUint); (hero, heroId) = PackingLib.unpackNftId(bytes32(packedHeroAsUint)); } epochBiomeData.freeUsers[userState.guildId].remove(user); epochData.pvpUserState[user] = IPvpController.PvpUserState({ activeFightIndex1: 0, biome: 0, guildId: 0, numHeroesStaked: userState.numHeroesStaked, countFights: 0, maxFights: 0, fightId: 0 }); emit IApplicationEvents.PvpHeroRemoved(user, userState.guildId, week, userState.biome, hero, heroId, manualRemoving); } /// @notice Change epoch if the current epoch is completed, update biome owner /// @param random_ CalcLib.pseudoRandom, required for unit tests function updateEpoch( uint8 biome, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal { // no restrictions to call _updateEpoch(biome, blockTimestamp, random_); } /// @notice Update epoch if necessary and get biome tax that takes into current biome owner /// @return guildId Owner of the biome /// @return taxPercent Tax percent in favor of the biome owner. [0...100_000], decimals 3 function refreshBiomeTax( uint8 biome, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal returns (uint guildId, uint taxPercent) { // no restrictions to call though this method is intended to be called by DungeonFactory IPvpController.BiomeData memory biomeData = _updateEpoch(biome, blockTimestamp, random_); return _getBiomeTax(biomeData); } /// @notice Called by GuildController when the guild is deleted function onGuildDeletion(IController controller, uint guildId) internal { _onlyGuildController(controller); IPvpController.EpochData storage epochData = _S().epochData[getCurrentEpochWeek(block.timestamp)]; uint8 targetBiome = epochData.targetBiome[guildId]; if (targetBiome != 0) { // deleted guild cannot be selected for new fights anymore epochData.biomeGuilds[targetBiome].remove(guildId); } // deleted guild cannot own a biome anymore uint8 _ownedBiome = _S().ownedBiome[guildId]; if (_ownedBiome != 0) { delete _S().ownedBiome[guildId]; delete _S().biomeState[_ownedBiome]; } } //endregion ------------------------ Domination actions //region ------------------------ Domination internal /// @notice Finalize passed epoch, initialize first epoch. /// Detect a winner for biome if it's not detected yet. function _updateEpoch( uint8 biome, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal returns (IPvpController.BiomeData memory biomeData) { biomeData = _S().biomeState[biome]; uint32 week = getCurrentEpochWeek(blockTimestamp); if (biomeData.startedEpochWeek == 0) { // initialize first epoch biomeData.startedEpochWeek = week; _S().biomeState[biome] = biomeData; emit IApplicationEvents.FirstPvpEpoch(biome, week); } else { if (week != biomeData.startedEpochWeek) { // started epoch has passed, it's time to sum up the results uint[] memory guildIds = _S().epochData[biomeData.startedEpochWeek].biomeGuilds[biome].values(); // detect new biome owner uint guildBiomeOwnerId = _detectBiomeOwner(biome, biomeData.startedEpochWeek, guildIds, random_); if (guildBiomeOwnerId == 0) { // new biome owner is not detected .. keep previous one guildBiomeOwnerId = biomeData.guildBiomeOwnerId; } if (guildBiomeOwnerId != biomeData.guildBiomeOwnerId) { uint8 prevBiome = _S().ownedBiome[guildBiomeOwnerId]; // clear data for prev owner of the biome if (biomeData.guildBiomeOwnerId != 0) { delete _S().ownedBiome[biomeData.guildBiomeOwnerId]; } // clear previously owned biome if (prevBiome != 0) { _S().biomeState[prevBiome].guildBiomeOwnerId = 0; _S().biomeState[prevBiome].dominationCounter = 0; } // update ownedBiome _S().ownedBiome[guildBiomeOwnerId] = biome; } // update biome state biomeData = IPvpController.BiomeData({ guildBiomeOwnerId: uint64(guildBiomeOwnerId), startedEpochWeek: week, dominationCounter: guildBiomeOwnerId == biomeData.guildBiomeOwnerId && guildBiomeOwnerId != 0 ? biomeData.dominationCounter + uint16(week - biomeData.startedEpochWeek) // penalty for repeat domination : 0 }); _S().biomeState[biome] = biomeData; emit IApplicationEvents.UpdatePvpEpoch(biome, week, guildBiomeOwnerId); } } } /// @notice Select a winner - the guild with max number of points /// If several guilds have same number of points, the winner is selected randomly /// @param random_ CalcLib.pseudoRandom, required for unit tests /// @return guildBiomeOwnerId New guild biome owner. If new biome owner is not detected, prev biome owner is returned function _detectBiomeOwner( uint8 biome, uint week, uint[] memory guildIds, function (uint) internal view returns (uint) random_ ) internal view returns (uint guildBiomeOwnerId) { uint len = guildIds.length; uint[] memory guildPoints = new uint[](len); uint selectedIndexP1; // 1-based index of the first winner guild in {guildPoints} uint countWinners; // total count of winners - the guilds with equal max number of points // find all winners - the guilds with max number of points IPvpController.EpochBiomeData storage epochBiomeData = _S().epochData[week].epochBiomeData[biome]; for (uint i; i < len; ++i) { (bool exist, uint countPoints) = epochBiomeData.guildPoints.tryGet(guildIds[i]); if (exist && countPoints != 0) { guildPoints[i] = countPoints; if (selectedIndexP1 == 0 || guildPoints[selectedIndexP1 - 1] < countPoints) { selectedIndexP1 = i + 1; countWinners = 1; } else if (guildPoints[selectedIndexP1 - 1] == countPoints) { countWinners++; } } } // select random winner from all potential winners if (selectedIndexP1 != 0) { uint indexWinner = countWinners == 1 ? 0 : random_(countWinners - 1); if (indexWinner == 0) { guildBiomeOwnerId = guildIds[selectedIndexP1 - 1]; } else { for (uint i = selectedIndexP1; i < len; ++i) { if (guildPoints[i] == guildPoints[selectedIndexP1 - 1]) { if (indexWinner == 1) { guildBiomeOwnerId = guildIds[i]; break; } else { indexWinner--; } } } } } return guildBiomeOwnerId; } /// @notice Try to find pvp-opponent for the hero. /// @param heroGuildId Guild of the hero /// @param random_ CalcLib.pseudoRandom, required for unit tests function _findPvpOpponent( IGuildController guildController, uint8 biome, IPvpController.EpochData storage epochData, uint heroGuildId, function (uint) internal view returns (uint) random_ ) internal view returns ( address opponentUser, bytes32 opponentPackedHero, uint opponentGuildId ) { (,,,uint8 guildLevel,,) = guildController.getGuildData(heroGuildId); if (guildLevel >= MIN_GUILD_LEVEL_REQUIRED_FOR_PVP) { IPvpController.EpochBiomeData storage epochBiomeData = epochData.epochBiomeData[biome]; opponentGuildId = _selectPvpOpponentGuild(guildController, epochData.biomeGuilds[biome], epochBiomeData, heroGuildId, random_); if (opponentGuildId != 0) { (opponentUser, opponentPackedHero) = _selectPvpOpponent(epochBiomeData, opponentGuildId, random_); if (opponentPackedHero == 0) revert IAppErrors.ZeroAddress(); // Pvp-fight is initialized, but not started // One of the users should start the fight manually } } return (opponentUser, opponentPackedHero, opponentGuildId); } /// @notice Prepare the fight: hero vs opponent. /// Both the hero and his opponent are prepared to the fight in result, but the fight is not started. /// @param heroOwner Owner of the selected hero function _setupPvpFight( uint8 biome, IPvpController.EpochData storage epochData, address heroOwner, address opponentUser, uint heroGuildId, uint opponentGuildId, SetupPvpFightParams memory eventParams ) internal { // Set up the fight between the hero and his opponent epochData.fightData[heroOwner].push(IPvpController.PvpFightData({ fightOpponent: opponentUser, fightStatus: IPvpController.PvpFightStatus.PREPARED_1, health: 0, countTurns: 0, mana: 0 })); epochData.fightData[opponentUser].push(IPvpController.PvpFightData({ fightOpponent: heroOwner, fightStatus: IPvpController.PvpFightStatus.PREPARED_1, health: 0, countTurns: 0, mana: 0 })); // update users states uint48 fightId = _generateFightId(); epochData.pvpUserState[heroOwner].activeFightIndex1 = uint32(epochData.fightData[heroOwner].length); epochData.pvpUserState[heroOwner].fightId = fightId; epochData.pvpUserState[opponentUser].activeFightIndex1 = uint32(epochData.fightData[opponentUser].length); epochData.pvpUserState[opponentUser].fightId = fightId; // remove free users (assume, that remove doesn't revert if user is not there) epochData.epochBiomeData[biome].freeUsers[heroGuildId].remove(heroOwner); epochData.epochBiomeData[biome].freeUsers[opponentGuildId].remove(opponentUser); (address opponentHero, uint opponentHeroId) = PackingLib.unpackNftId(bytes32(eventParams.opponentPackedHero)); emit IApplicationEvents.PreparePvpFight( fightId, eventParams.week, eventParams.hero, eventParams.heroId, heroGuildId, opponentHero, opponentHeroId, opponentGuildId ); } /// @notice Select random guild suitable to select pvp-opponent. /// The guild should have at least 1 free hero. The guild should have enough level. /// Relation between the guilds of the selected opponents should be "war". /// The opponents should belong to the different guilds. function _selectPvpOpponentGuild( IGuildController guildController, EnumerableSet.UintSet storage biomeGuilds, IPvpController.EpochBiomeData storage epochBiomeData, uint heroGuildId, function (uint) internal view returns (uint) random_ ) internal view returns (uint resultGuildId) { // select first guild randomly // enumerate all guilds one by one (loop) and find first guild with available free hero uint len = biomeGuilds.length(); // biomeGuilds should have at least two guild: hero's guild and opponent's guild // if there is only 1 guild it means that this is hero's guild and no opponent is available if (len > 1) { uint index0 = random_(len - 1); uint index = index0; while (true) { uint guildId = biomeGuilds.at(index); (,,,uint8 guildLevel,,) = guildController.getGuildData(guildId); if (guildLevel >= MIN_GUILD_LEVEL_REQUIRED_FOR_PVP && epochBiomeData.freeUsers[guildId].length() != 0 && heroGuildId != guildId && !guildController.isPeacefulRelation(heroGuildId, guildId) ) { resultGuildId = guildId; break; } index = index + 1 == len ? 0 : index + 1; // loop if (index == index0) { // guild wasn't found break; } } } return resultGuildId; } /// @notice Select random pvp-opponent in the given {guild} function _selectPvpOpponent( IPvpController.EpochBiomeData storage epochBiomeData, uint guildId, function (uint) internal view returns (uint) random_ ) internal view returns (address user, bytes32 packedHero) { EnumerableSet.AddressSet storage freeUsers = epochBiomeData.freeUsers[guildId]; uint len = freeUsers.length(); if (len != 0) { uint index = len == 1 ? 0 : random_(len - 1); user = freeUsers.at(index); packedHero = bytes32(epochBiomeData.registeredHeroes[guildId].get(user)); } return (user, packedHero); } /// @return guildId Guild to which {msgSender} belongs, revert if 0 /// @return targetBiome Domination biome of the guild, revert if 0 function _getTargetDominationBiomeWithCheck(address msgSender, IGuildController guildController, uint32 week, bool revertOnZero) internal view returns ( uint guildId, uint8 targetBiome ) { guildId = guildController.memberOf(msgSender); if (revertOnZero && guildId == 0) revert IAppErrors.NotGuildMember(); targetBiome = guildId == 0 ? 0 : _S().epochData[week].targetBiome[guildId]; if (revertOnZero && targetBiome == 0) revert IAppErrors.NoDominationRequest(); } /// @notice Get biome tax that takes into account extra fee ratio provided by GuildStakingAdapter /// @return guildId Owner of the biome /// @return taxPercent Final tax percent that takes into account possible penalty. [0...100_000], decimals 3 function _getBiomeTax(IPvpController.BiomeData memory biomeData) internal view returns (uint guildId, uint taxPercent) { guildId = biomeData.guildBiomeOwnerId; taxPercent = guildId == 0 ? 0 : AppLib.BIOME_TAX_PERCENT_MIN; if (guildId != 0) { // increment tax value depending on the liquidity amount staked by the guild address guildStakingAdapter = address(uint160(_S().pvpParam[IPvpController.PvpParams.GUILD_STAKING_ADAPTER_2])); if (guildStakingAdapter != address(0)) { // staked amount in game token uint extraFeeRatio = IGuildStakingAdapter(guildStakingAdapter).getExtraFeeRatio(guildId); taxPercent += (AppLib.BIOME_TAX_PERCENT_MAX - AppLib.BIOME_TAX_PERCENT_MIN) * Math.min(extraFeeRatio, 1e18) / 1e18; } } } //endregion ------------------------ Domination internal //region ------------------------ Utils function getCurrentEpochWeek(uint blockTimestamp) internal pure returns (uint32) { return _getEpochWeek(uint32(blockTimestamp / 86400)); } /// @notice Calculate week for the given day. Assume that first day of the week is Monday function _getEpochWeek(uint epochDay) internal pure returns (uint32) { return uint32((epochDay + 3) / 7); // + 3 to move start of the first week to Monday 1969-12-29 } /// @notice Check if the {user} has given permission in the guild. Permissions are specified by bitmask {rights}. /// Admin is marked by zero bit, he has all permissions always. function _checkPermissions(IGuildController guildController, address user, IGuildController.GuildRightBits right) internal view returns (uint guildId, uint rights) { guildId = guildController.memberOf(user); rights = guildController.getRights(user); if (guildId == 0) revert IAppErrors.NotGuildMember(); if (!( (rights & (2 ** uint(IGuildController.GuildRightBits.ADMIN_0))) != 0 || (rights & (2 ** uint(right))) != 0 )) { revert IAppErrors.GuildActionForbidden(uint(right)); } } function _getPointsWithPenalty(uint points_, uint dominationCounter) internal pure returns (uint) { uint penalty; if (dominationCounter != 0) { if (dominationCounter == 1) penalty = 10; else if (dominationCounter == 2) penalty = 25; else if (dominationCounter == 3) penalty = 38; else if (dominationCounter == 4) penalty = 50; else if (dominationCounter == 5) penalty = 61; else if (dominationCounter == 6) penalty = 70; else if (dominationCounter == 7) penalty = 78; else if (dominationCounter == 8) penalty = 84; else if (dominationCounter == 9) penalty = 89; else if (dominationCounter == 10) penalty = 93; else if (dominationCounter == 11) penalty = 96; else penalty = 98; } return points_ * (100 - penalty) / 100; } /// @notice Generate unique id of the pvp-fight (each pvp-fight consists from multiple turns) function _generateFightId() internal returns (uint48 fightId) { fightId = 1 + uint48(_S().pvpParam[IPvpController.PvpParams.FIGHT_COUNTER_3]); _S().pvpParam[IPvpController.PvpParams.FIGHT_COUNTER_3] = fightId; } //endregion ------------------------ Utils }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IERC721.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IPvpController.sol"; import "../interfaces/IReinforcementController.sol"; import "../interfaces/IGuildStakingAdapter.sol"; import "./CalcLib.sol"; import "./ControllerContextLib.sol"; import "./ScoreLib.sol"; import "./MonsterLib.sol"; import "./AppLib.sol"; import "./PackingLib.sol"; import "./PvpControllerLib.sol"; import "./PvpAttackLib.sol"; import "./ReinforcementControllerLib.sol"; library PvpFightLib { using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMap for EnumerableMap.AddressToUintMap; using EnumerableMap for EnumerableMap.UintToUintMap; //region ------------------------ Constants /// @notice Max total count of turns per pvp-fight uint internal constant MAX_COUNT_TURNS = 100; /// @notice Heroes do real damage to each other in PVP (items can be broken, hp/mp can be reduced, loser looses 1 life chance) bool internal constant REAL_DAMAGE_IN_FIGHTS = true; //endregion ------------------------ Constants //region ------------------------ Data types struct HeroContext { bool isWinner; IPvpController.PvpFightStatus fightStatus; /// @notice Target domination biome of the hero's guild uint8 targetBiome; uint32 statLife; uint32 statMana; /// @notice Score of the hero's opponent received by the hero's guild uint prize; IPvpController.HeroData heroData; IStatController.ChangeableStats stats; IPvpController.PvpUserState userState; } struct StartFightContext { bool technicalDefeat; uint32 week; IGuildController guildController; IStatController statController; IItemController itemController; IUserController userController; IHeroController heroController; HeroContext hero; HeroContext opponent; IPvpController.PvpFightResults fightResult; } struct PrepareFightLocal { uint8 biome; uint32 week; address opponent; IGuildController guildController; bytes32 opponentPackedHero; uint opponentGuildId; } struct SavePvpResultsOnCompletionLocal { bool alive; bool keepStaked; bool died; } struct SaveFightResultsInput { address user; address otherUser; bool isHero; } //endregion ------------------------ Data types //region ------------------------ PvP actions /// @notice Find opponent for the user's hero, prepare the fight /// @dev Normally the fight is prepared automatically on hero registration. /// In some cases it doesn't happen and the preparation should be made manually. /// Ex: Two guilds had peaceful relation. Users of the both guilds registered their heroes. /// As soon as there are no other guilds, the fights are not initialized. /// The guilds change their relation to "war". Now users should initialize the fights manually. /// @param random_ CalcLib.pseudoRandom, required for unit tests function prepareFight( address msgSender, IController controller, uint blockTimestamp, function (uint) internal view returns (uint) random_ ) internal { PvpControllerLib._onlyNotPaused(controller); PrepareFightLocal memory v; v.guildController = IGuildController(controller.guildController()); v.week = PvpControllerLib.getCurrentEpochWeek(blockTimestamp); (, v.biome) = PvpControllerLib._getTargetDominationBiomeWithCheck(msgSender, v.guildController, v.week, true); IPvpController.EpochData storage epochData = PvpControllerLib._S().epochData[v.week]; IPvpController.PvpUserState memory userState = epochData.pvpUserState[msgSender]; PvpControllerLib._onlyUserWithRegisteredPvpHeroWithoutFights(userState); (v.opponent, v.opponentPackedHero, v.opponentGuildId) = PvpControllerLib._findPvpOpponent(v.guildController, v.biome, epochData, userState.guildId, random_); if (v.opponent == address(0)) revert IAppErrors.PvpFightOpponentNotFound(); IPvpController.HeroData memory heroData = _getHeroData(epochData.epochBiomeData[userState.biome], userState.guildId, msgSender); PvpControllerLib._setupPvpFight(v.biome, epochData, msgSender, v.opponent, userState.guildId, v.opponentGuildId, PvpControllerLib.SetupPvpFightParams({ week: v.week, hero: heroData.hero, heroId: heroData.heroId, opponentPackedHero: v.opponentPackedHero })); } /// @notice Start set of fight turns. Each set can include no more than {maxCountTurns} turns. /// The whole fight = {set of turns}, set of turn = {turns}, 1 transaction = 1 set of turns. /// Heroes must fight until one of the heroes has zero lives left. /// Total number of turns cannot exceed {MAX_COUNT_TURNS}. /// In this case, the hero with the most lives is chosen as the winner. function startFight(address msgSender, IController controller, uint blockTimestamp, uint8 maxCountTurns) external { _doMultipleTurns(msgSender, controller, blockTimestamp, maxCountTurns, CalcLib.pseudoRandom, _pvpFight); } //endregion ------------------------ PvP actions //region ------------------------ PvP fight /// @dev Wrapper function for {PvpAttackLib.pvpFight} to be able to pass it into {_doMultipleTurns} function _pvpFight( PvpAttackLib.PvpFightParams memory p, IPvpController.HeroData memory hero, IPvpController.HeroData memory opponent ) internal returns (IPvpController.PvpFightResults memory fightResult) { return PvpAttackLib.pvpFight(p, hero, opponent); } /// @notice Execute no more than {maxCountTurns} turns /// @param maxCountTurns Max number of turns that can be performed during this call /// @param random_ Pass _pseudoRandom here, the param is required to simplify unit testing /// @param fight_ Pass {_pvpFight} here, the param is required to simplify unit testing function _doMultipleTurns( address msgSender, IController controller, uint blockTimestamp, uint8 maxCountTurns, function (uint) internal view returns (uint) random_, function ( PvpAttackLib.PvpFightParams memory, IPvpController.HeroData memory, IPvpController.HeroData memory ) internal returns (IPvpController.PvpFightResults memory) fight_ ) internal { PvpControllerLib._onlyNotPaused(controller); StartFightContext memory v; v.itemController = IItemController(controller.itemController()); v.userController = IUserController(controller.userController()); v.guildController = IGuildController(controller.guildController()); v.statController = IStatController(controller.statController()); v.heroController = IHeroController(controller.heroController()); v.week = PvpControllerLib.getCurrentEpochWeek(blockTimestamp); IPvpController.EpochData storage epochData = PvpControllerLib._S().epochData[v.week]; // set up v.XXX.biome, v.XXX.userState and v.XXX.fightData for each hero IPvpController.PvpFightData memory heroFightData = _initBiomeUserStateFightData(msgSender, v, epochData, true); IPvpController.PvpFightData memory opponentFightData = _initBiomeUserStateFightData(heroFightData.fightOpponent, v, epochData, false); IPvpController.EpochBiomeData storage epochBiomeData = epochData.epochBiomeData[v.hero.userState.biome]; // set up v.XXX.heroData and v.XXX.stats for each hero _initStatsHeroData(msgSender, v, epochBiomeData, heroFightData, true); _initStatsHeroData(heroFightData.fightOpponent, v, epochBiomeData, opponentFightData, false); // ---------------- technical defeat // pvp-fight was started then the guild has changed domination request to different biome // pvp-fight is continued, but now fight's biome is different from target one => technical defeat of the hero // such situation is possible for any hero and even for the both at the same time (v.technicalDefeat, v.hero.isWinner) = _checkTechnicalDefeat( v.hero.userState.biome != v.hero.targetBiome, v.opponent.userState.biome != v.opponent.targetBiome, random_ ); // ---------------- fights if (v.technicalDefeat) { v.opponent.isWinner = !v.hero.isWinner; v.fightResult = IPvpController.PvpFightResults({ healthHero: v.hero.isWinner ? v.hero.stats.life : 0, healthOpponent: v.opponent.isWinner ? v.opponent.stats.life : 0, completed: true, totalCountFights: heroFightData.countTurns, manaConsumedHero: v.hero.isWinner ? 0 : v.hero.stats.mana, manaConsumedOpponent: v.opponent.isWinner ? 0 : v.opponent.stats.mana }); } else { v.fightResult = fight_( PvpAttackLib.PvpFightParams({ msgSender: msgSender, fightId: v.hero.userState.fightId, statController: v.statController, itemController: v.itemController, maxCountTurns: maxCountTurns, heroStats: v.hero.stats, opponentStats : v.opponent.stats, countTurnsMade: heroFightData.countTurns // opponent has exactly same count of turns }), v.hero.heroData, v.opponent.heroData ); if (v.fightResult.completed) { (v.hero.isWinner, v.opponent.isWinner) = _getWinners(v.fightResult, random_); } } // ---------------- save results _saveFightResults(SaveFightResultsInput(msgSender, heroFightData.fightOpponent, true), v, epochData, epochBiomeData); _saveFightResults(SaveFightResultsInput(heroFightData.fightOpponent, msgSender, false), v, epochData, epochBiomeData); if (v.fightResult.completed) { _emitFightCompleted(v); } } /// @notice Initialize v.XXX.heroData, v.XXX.stats, v.XXX.statLife/Mana for the given hero function _initStatsHeroData( address user, StartFightContext memory v, IPvpController.EpochBiomeData storage epochBiomeData, IPvpController.PvpFightData memory fightData, bool isHero ) internal view { HeroContext memory h = isHero ? v.hero : v.opponent; // v.opponentGuildId can be 0 if the opponent was removed from the guild // in this case opponentData will contain zeros too => technical defeat with looserScore = 0 h.heroData = _getHeroData(epochBiomeData, h.userState.guildId, user); // get hero and opponent states h.stats = v.statController.heroStats(h.heroData.hero, h.heroData.heroId); // store current stat-values - these are values for the moment before starting the fight h.statLife = h.stats.life; h.statMana = h.stats.mana; // override life and mana in hero state if the fight has been started and now it is being continued if (fightData.fightStatus == IPvpController.PvpFightStatus.FIGHTING_2) { h.stats.life = fightData.health; h.stats.mana = fightData.mana; } } /// @notice Initialize v.XXX.biome, v.XXX.userState and fightData for the given hero function _initBiomeUserStateFightData(address user, StartFightContext memory v, IPvpController.EpochData storage epochData, bool isHero) internal view returns ( IPvpController.PvpFightData memory heroFightData ){ HeroContext memory h = isHero ? v.hero : v.opponent; (, h.targetBiome) = PvpControllerLib._getTargetDominationBiomeWithCheck(user, v.guildController, v.week, isHero); h.userState = epochData.pvpUserState[user]; if (h.userState.activeFightIndex1 == 0) { // ensure that the fighting is prepared and not completed revert IAppErrors.PvpFightIsNotPrepared(h.targetBiome, v.week, user); } heroFightData = PvpControllerLib.getFightDataByIndex(v.week, user, h.userState.activeFightIndex1 - 1); if ( heroFightData.fightStatus == IPvpController.PvpFightStatus.WINNER_3 || heroFightData.fightStatus == IPvpController.PvpFightStatus.LOSER_4 ) revert IAppErrors.PvpFightIsCompleted(h.targetBiome, v.week, user); } function _saveFightResults( SaveFightResultsInput memory p, StartFightContext memory v, IPvpController.EpochData storage epochData, IPvpController.EpochBiomeData storage epochBiomeData ) internal { HeroContext memory h = p.isHero ? v.hero : v.opponent; HeroContext memory other = p.isHero ? v.opponent : v.hero; if (v.fightResult.completed) { // update final state (the fight is completed) h.fightStatus = h.isWinner ? IPvpController.PvpFightStatus.WINNER_3 : IPvpController.PvpFightStatus.LOSER_4; // looser always lost all mp and hp if (!h.isWinner) { if (p.isHero) { v.fightResult.healthHero = 0; v.fightResult.manaConsumedHero = v.hero.stats.mana; } else { v.fightResult.healthOpponent = 0; v.fightResult.manaConsumedOpponent = v.opponent.stats.mana; } } // update winner guild points for biome domination // Possible edge cases: hero/opponent is the winner, but his guild has different target now. // In such cases guild doesn't receive guild points of the winner h.prize = h.isWinner && h.userState.biome == h.targetBiome // apply penalty for repeat domination to the the number of pvp-points ? PvpControllerLib._getPointsWithPenalty( v.heroController.score(other.heroData.hero, other.heroData.heroId), PvpControllerLib._S().biomeState[h.targetBiome].dominationCounter ) : 0; _savePvpResultsOnCompletion(p, v, epochData, epochBiomeData, REAL_DAMAGE_IN_FIGHTS); // reset all buffs and clear usage of the consumables v.statController.clearTemporallyAttributes(h.heroData.hero, h.heroData.heroId); v.statController.clearUsedConsumables(h.heroData.hero, h.heroData.heroId); } else { // update intermediate state (the fight is not completed, new set of fight is required) h.fightStatus = IPvpController.PvpFightStatus.FIGHTING_2; } epochData.fightData[p.user][h.userState.activeFightIndex1 - 1] = IPvpController.PvpFightData({ fightStatus: h.fightStatus, fightOpponent: p.otherUser, countTurns: v.fightResult.totalCountFights, health: p.isHero ? v.fightResult.healthHero : v.fightResult.healthOpponent, mana: AppLib.sub0(h.stats.mana, p.isHero ? v.fightResult.manaConsumedHero : v.fightResult.manaConsumedOpponent) }); } /// @notice Save results of completed PVP fight: update guild points, register daily activity, update core stat, /// reduce durability, add the winner to the list of heroes free for fight. /// @param realDamageAllowed For tests, the value is REAL_DAMAGE_IN_FIGHTS function _savePvpResultsOnCompletion( SaveFightResultsInput memory p, StartFightContext memory c, IPvpController.EpochData storage epochData, IPvpController.EpochBiomeData storage epochBiomeData, bool realDamageAllowed ) internal { SavePvpResultsOnCompletionLocal memory v; HeroContext memory h = p.isHero ? c.hero : c.opponent; uint32 fightResultHealth = p.isHero ? c.fightResult.healthHero : c.fightResult.healthOpponent; uint32 fightResultConsumedMana = p.isHero ? c.fightResult.manaConsumedHero : c.fightResult.manaConsumedOpponent; // update guild points counter for the guild of the winner if (h.isWinner) { (bool exist, uint guildPoints) = epochBiomeData.guildPoints.tryGet(h.userState.guildId); epochBiomeData.guildPoints.set(h.userState.guildId, (exist ? guildPoints : 0) + h.prize); // update global guild points counter // assume here, that uint64 is enough to store any sums of scores if (h.prize != 0) { c.guildController.incPvpCounter(h.userState.guildId, uint64(h.prize)); } } // both users register daily activity c.userController.registerPvP(p.user, h.isWinner); v.alive = h.isWinner; // winner is kept staked OR can be auto-removed if maxFights-limit is reached // loser's hero is auto-removed always and should be staked again v.keepStaked = v.alive && (h.userState.maxFights == 0 || h.userState.maxFights > h.userState.countFights + 1); v.died = !v.alive && h.stats.lifeChances <= 1; if (realDamageAllowed) { // update hp, mp, lc if (fightResultHealth == 0 && v.alive) { // winner has new life = 0 => he loses life chance in the same way as the looser and should be removed v.alive = false; v.keepStaked = false; } if (!v.died) { { // decrease life, mana and probably life-chance IStatController.ChangeableStats memory cs = IStatController.ChangeableStats({ level: 0, experience: 0, life: AppLib.sub0(h.statLife, fightResultHealth), // the fight consists from many turns, here: // v.heroStatMana = mana before starting the fight (== starting the turn) // v.heroStats.mana = mana at the moment of the beginning current set of fights // v.fightResult.manaConsumedHero = mana consumed during current set of fights // mana = total value of mana consumed from starting the fight mana: AppLib.sub0(h.statMana, (AppLib.sub0(h.stats.mana, fightResultConsumedMana))), lifeChances: v.alive ? 0 : 1 }); c.statController.changeCurrentStats(h.heroData.hero, h.heroData.heroId, cs, false); } if (!v.alive) { // hero has lost once life chance, but result life chance > 0 => restore life and mana IStatController.ChangeableStats memory cs = IStatController.ChangeableStats({ level: 0, experience: 0, life: _getHeroAttribute(c.statController, h, IStatController.ATTRIBUTES.LIFE), mana: _getHeroAttribute(c.statController, h, IStatController.ATTRIBUTES.MANA), lifeChances: 0 }); c.statController.changeCurrentStats(h.heroData.hero, h.heroData.heroId, cs, true); } } // reduce durability of all equipped items (including all skills), take off broken items c.itemController.reduceDurability(h.heroData.hero, h.heroData.heroId, h.userState.biome, true); } if (v.keepStaked) { // update user state epochData.pvpUserState[p.user] = IPvpController.PvpUserState({ activeFightIndex1: 0, biome: h.userState.biome, guildId: h.userState.guildId, numHeroesStaked: epochData.pvpUserState[p.user].numHeroesStaked, countFights: h.userState.countFights + 1, // overflow is not possible, see keepStaked above maxFights: h.userState.maxFights, fightId: 0 // there is no active fight anymore }); // add the (only live) winner back to the list of the users free for pvp epochBiomeData.freeUsers[h.userState.guildId].add(p.user); } else { PvpControllerLib._removePvpHero(c.week, epochData, epochBiomeData, h.userState, p.user, false); } if (realDamageAllowed && v.died) { _killHero(c.heroController, c.guildController, h, (p.isHero ? c.opponent : c.hero).userState.guildId); } } function _getHeroAttribute(IStatController statController, HeroContext memory h, IStatController.ATTRIBUTES attribute) internal view returns (uint32) { return uint32(uint(int(statController.heroAttribute(h.heroData.hero, h.heroData.heroId, uint(attribute))))); } /// @notice Kill the hero and send all items to the winner's guild bank function _killHero(IHeroController heroController, IGuildController guildController, HeroContext memory hero, uint opponentGuildId) internal { bytes32[] memory dropItems = heroController.kill(hero.heroData.hero, hero.heroData.heroId); uint len = dropItems.length; if (len != 0) { address guildBank = guildController.getGuildBank( hero.isWinner // the hero is winner but he is dead, all drop is sent to the bank of the hero's guild ? hero.userState.guildId // the hero is looser and he is dead, all drop is send to the bank of the opponent's guild : opponentGuildId ); if (guildBank == address(0)) revert IAppErrors.ZeroAddress(); // weird case, it should never appear address[] memory items = new address[](len); uint[] memory itemIds = new uint[](len); for (uint i; i < len; ++i) { (items[i], itemIds[i]) = PackingLib.unpackNftId(dropItems[i]); // SCR-1253: Attention: GuildBank with version below 1.0.2 was not inherited from ERC721Holder (mistake). // As result, safeTransferFrom doesn't work with such banks, they must be updated. So, use transferFrom here. IERC721(items[i]).transferFrom(address(this), guildBank, itemIds[i]); } emit IApplicationEvents.AddPvpFightItems(hero.userState.fightId, items, itemIds); } } /// @notice The hero with greater health is the winner. If healths are the same the winner is selected randomly /// If both heroes have zero lives assume that they are both winners. function _getWinners( IPvpController.PvpFightResults memory fightResult, function (uint) internal view returns (uint) random_ ) internal view returns (bool isHeroWinner, bool isOpponentWinner) { if (fightResult.healthHero > fightResult.healthOpponent) { isHeroWinner = true; } else if (fightResult.healthHero < fightResult.healthOpponent) { isOpponentWinner = true; } else { if (fightResult.healthHero == 0) { // special case: both heroes have zero health => they are both WINNERS isHeroWinner = true; isOpponentWinner = true; } else { // special case: both heroes have same NOT ZERO health => the winner is selected randomly isHeroWinner = random_(1) == 0; isOpponentWinner = !isHeroWinner; } } return (isHeroWinner, isOpponentWinner); } function _getHeroData(IPvpController.EpochBiomeData storage epochBiomeData, uint guildId, address user) internal view returns ( IPvpController.HeroData memory heroData ) { (bool exist, uint packedHeroAsInt) = epochBiomeData.registeredHeroes[guildId].tryGet(user); bytes32 packedHero = exist ? bytes32(packedHeroAsInt) : bytes32(0); // edge cases: i.e. opponent is not guild member and so guildId is 0 (heroData.hero, heroData.heroId) = PackingLib.unpackNftId(packedHero); heroData.pvpStrategy = epochBiomeData.pvpStrategy[packedHero]; return heroData; } /// @notice Check if technical defeat detected (the defeat without actual fighting) /// @param heroHasTechnicalDefeat Special case: the hero has technical defeat /// if he fights in biome X but his guild has changed domination request to biome Y /// @param opponentHasTechnicalDefeat Special case: the opponent has technical defeat /// if he fights in biome X but his guild has changed domination request to biome Y /// @return technicalDefeat Technical defeat is detected /// @return isHeroWinner Hero is the winner in the detected technical defeat function _checkTechnicalDefeat( bool heroHasTechnicalDefeat, bool opponentHasTechnicalDefeat, function (uint) internal view returns (uint) random_ ) internal view returns ( bool technicalDefeat, bool isHeroWinner ) { technicalDefeat = heroHasTechnicalDefeat || opponentHasTechnicalDefeat; if (technicalDefeat) { if (heroHasTechnicalDefeat) { if (opponentHasTechnicalDefeat) { // both heroes have technical defeats, the winner is selected randomly isHeroWinner = random_(1) == 0; } else { // the opponent is the winner, by default: isHeroWinner = false; } } else { isHeroWinner = true; } } return (technicalDefeat, isHeroWinner); } //endregion ------------------------ PvP fight //region ------------------------ Events function _emitFightCompleted(StartFightContext memory v) internal { emit IApplicationEvents.PvpFightCompleted( v.fightResult, v.hero.userState.fightId, [v.hero.heroData.hero, v.opponent.heroData.hero], [v.hero.userState.guildId, v.opponent.userState.guildId], [v.hero.isWinner, v.opponent.isWinner], [v.hero.prize, v.opponent.prize], v.technicalDefeat ); } //endregion ------------------------ Events }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/Math.sol"; import "../interfaces/IApplicationEvents.sol"; import "../interfaces/IDungeonFactory.sol"; import "../interfaces/IERC20.sol"; import "../interfaces/IERC721.sol"; import "../interfaces/IGuildController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IGameToken.sol"; import "../interfaces/IMinter.sol"; import "../interfaces/IOracle.sol"; import "../interfaces/ITreasury.sol"; import "../interfaces/IReinforcementController.sol"; import "../lib/CalcLib.sol"; import "../lib/PackingLib.sol"; import "../lib/AppLib.sol"; library ReinforcementControllerLib { using EnumerableSet for EnumerableSet.Bytes32Set; using EnumerableMap for EnumerableMap.AddressToUintMap; using EnumerableMap for EnumerableMap.Bytes32ToUintMap; using PackingLib for bytes32; using PackingLib for address; using PackingLib for uint8[]; //region ------------------------ Constants /// @dev keccak256(abi.encode(uint256(keccak256("reinforcement.controller.main")) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant MAIN_STORAGE_LOCATION = 0x5a053c541e08c6bd7dfc3042a100e83af246544a23ecda1a47bf22b441b00c00; uint internal constant _SEARCH_WINDOW = 100; int32 internal constant _ATTRIBUTES_RATIO = 20; uint internal constant _FEE_MIN = 10; uint internal constant _TO_HELPER_RATIO_MAX = 50; uint internal constant _STAKE_REDUCE_DELAY = 7 days; uint internal constant _DELAY_FACTOR = 2; uint internal constant _SIP001_COUNT_REQUIRED_SKILLS = 3; /// @notice Min level of shelter where guild reinforcement is allowed. 2, 3 - allowed, 1 - forbidden. uint internal constant MIN_SHELTER_LEVEL_GUILD_REINFORCEMENT_ALLOWED = 2; /// @notice Guild hero staking is not allowed during following period after withdrawing the hero uint internal constant HERO_COOLDOWN_PERIOD_AFTER_GUILD_HERO_WITHDRAWING = 1 days; uint internal constant STATUS_HELPER_FREE = 0; /// @notice 24 hours is divided on "baskets". Each basket covers given interval of the hours. uint constant internal BASKET_INTERVAL = 3; //endregion ------------------------ Constants //region ------------------------ Restrictions function onlyHeroController(IController controller) internal view returns (address heroController){ heroController = controller.heroController(); if (heroController != msg.sender) revert IAppErrors.ErrorNotHeroController(msg.sender); } /// @notice Ensure that the user is a member of a guild, the guild has a shelter and the shelter has level > 1 function onlyGuildWithShelterEnoughLevel(IGuildController gc, uint guildId) internal view { uint shelterId = gc.guildToShelter(guildId); if (shelterId == 0) revert IAppErrors.GuildHasNoShelter(); (, uint8 shelterLevel, ) = PackingLib.unpackShelterId(shelterId); if (shelterLevel < MIN_SHELTER_LEVEL_GUILD_REINFORCEMENT_ALLOWED) revert IAppErrors.ShelterHasNotEnoughLevelForReinforcement(); } function onlyNotPausedEoaOwner(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal view { if (!isEoa) revert IAppErrors.ErrorOnlyEoa(); if (controller.onPause()) revert IAppErrors.ErrorPaused(); if (IERC721(heroToken).ownerOf(heroId) != msgSender) revert IAppErrors.ErrorNotOwner(heroToken, heroId); } function onlyDungeonFactory(IController controller) internal view { if (controller.dungeonFactory() != msg.sender) revert IAppErrors.ErrorNotDungeonFactory(msg.sender); } function onlyDeployer(IController controller) internal view { if (!controller.isDeployer(msg.sender)) revert IAppErrors.ErrorNotDeployer(msg.sender); } function _checkStakeAllowed(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal view returns (IHeroController){ onlyNotPausedEoaOwner(isEoa, controller, msgSender, heroToken, heroId); IHeroController hc = IHeroController(controller.heroController()); if (hc.heroClass(heroToken) == 0) revert IAppErrors.ErrorHeroIsNotRegistered(heroToken); if (IDungeonFactory(controller.dungeonFactory()).currentDungeon(heroToken, heroId) != 0) revert IAppErrors.HeroInDungeon(); if (isStaked(heroToken, heroId)) revert IAppErrors.AlreadyStaked(); address pvpController = controller.pvpController(); if (pvpController != address(0)) { if (IPvpController(pvpController).isHeroStakedCurrently(heroToken, heroId)) revert IAppErrors.PvpStaked(); } if (hc.sandboxMode(heroToken, heroId) == uint8(IHeroController.SandboxMode.SANDBOX_MODE_1)) revert IAppErrors.SandboxModeNotAllowed(); return hc; } function _checkWithdrawAllowed(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal view { onlyNotPausedEoaOwner(isEoa, controller, msgSender, heroToken, heroId); if (IHeroController(controller.heroController()).heroClass(heroToken) == 0) revert IAppErrors.ErrorHeroIsNotRegistered(heroToken); } function _memberOf(IController controller, address msgSender) internal view returns (IGuildController gc, uint guildId) { gc = IGuildController(controller.guildController()); guildId = gc.memberOf(msgSender); if (guildId == 0) revert IAppErrors.NotGuildMember(); } //endregion ------------------------ Restrictions //region ------------------------ VIEWS function _S() internal pure returns (IReinforcementController.MainState storage s) { assembly { s.slot := MAIN_STORAGE_LOCATION } return s; } function toHelperRatio(IController controller, address heroToken, uint heroId) internal view returns (uint) { // Assume that this function is called by dungeonLib before reinforcement releasing // so for guild-reinforcement we can detect guild by guildHelperOf uint guildId = busyGuildHelperOf(heroToken, heroId); if (guildId == 0) { // Helper doesn't receive any reward at the end of the dungeon in Reinforcement V2 // fixed reward-amount is paid to the helper in askHeroV2, that's all return 0; } else { // guild reinforcement // assume that guildController is initialized (, , , , , uint _toHelperRatio) = IGuildController(controller.guildController()).getGuildData(guildId); return _toHelperRatio; } } function heroInfo(address heroToken, uint heroId) internal view returns (IReinforcementController.HeroInfo memory) { return unpackHeroInfo(_S()._stakedHeroes[heroToken.packNftId(heroId)]); } function heroInfoV2(address heroToken, uint heroId) internal view returns (IReinforcementController.HeroInfoV2 memory) { return _S().stakedHeroesV2[heroToken.packNftId(heroId)]; } /// @notice Check if the hero is staked using classic or guild reinforcement function isStaked(address heroToken, uint heroId) internal view returns (bool) { return isStakedV2(heroToken, heroId) || getStakedHelperGuild(heroToken, heroId) != 0 || isStakedV1(heroToken, heroId); } function isStakedV1(address heroToken, uint heroId) internal view returns (bool) { return heroInfo(heroToken, heroId).biome != 0; } function isStakedV2(address heroToken, uint heroId) internal view returns (bool) { return heroInfoV2(heroToken, heroId).biome != 0; } /// @return Return the guild in which the hero is staked for guild reinforcement function getStakedHelperGuild(address heroToken, uint heroId) internal view returns (uint) { return _S().stakedGuildHeroes[heroToken.packNftId(heroId)]; } function stakedGuildHelpersLength(uint guildId) internal view returns (uint) { return _S().guildHelpers[guildId].length(); } function stakedGuildHelperByIndex(uint guildId, uint index) internal view returns ( address helper, uint helperId, uint busyInGuildId ) { bytes32 packedHelper; (packedHelper, busyInGuildId) = _S().guildHelpers[guildId].at(index); (helper, helperId) = PackingLib.unpackNftId(packedHelper); } function earned(address heroToken, uint heroId) internal view returns ( address[] memory tokens, uint[] memory amounts, address[] memory nfts, uint[] memory ids ){ EnumerableMap.AddressToUintMap storage erc20Rewards = _S()._heroTokenRewards[heroToken.packNftId(heroId)]; uint length = erc20Rewards.length(); tokens = new address[](length); amounts = new uint[](length); for (uint i; i < length; ++i) { (tokens[i], amounts[i]) = erc20Rewards.at(i); } bytes32[] storage nftRewards = _S()._heroNftRewards[heroToken.packNftId(heroId)]; length = nftRewards.length; nfts = new address[](length); ids = new uint[](length); for (uint i; i < length; ++i) { (nfts[i], ids[i]) = PackingLib.unpackNftId(nftRewards[i]); } } /// @notice Return the guild in which the hero is currently asked for guild reinforcement function busyGuildHelperOf(address heroToken, uint heroId) internal view returns (uint guildId) { return _S().busyGuildHelpers[heroToken.packNftId(heroId)]; } /// @notice Return moment of last withdrawing of the hero from guild reinforcement function lastGuildHeroWithdrawTs(address heroToken, uint heroId) internal view returns (uint guildId) { return _S().lastGuildHeroWithdrawTs[heroToken.packNftId(heroId)]; } function getConfigV2() internal view returns (uint32 minNumberHits, uint32 maxNumberHits, uint32 lowDivider, uint32 highDivider, uint8 levelLimit) { return PackingLib.unpackConfigReinforcementV2( bytes32(_S().configParams[IReinforcementController.ConfigParams.V2_MIN_MAX_BOARD_0]) ); } function getFeeAmount(address gameToken, uint hitsLast24h, uint8 biome) internal view returns (uint feeAmount) { return _getFeeAmount(gameToken, hitsLast24h, biome); } function getHitsNumberPerLast24Hours(uint8 biome, uint blockTimestamp) internal view returns (uint hitsLast24h) { IReinforcementController.LastWindowsV2 memory stat24h = _S().stat24hV2[biome]; (hitsLast24h, ) = getHitsNumberPerLast24Hours(blockTimestamp, BASKET_INTERVAL, stat24h); } function getLastWindowsV2(uint8 biome) internal view returns (IReinforcementController.LastWindowsV2 memory) { return _S().stat24hV2[biome]; } function heroesByBiomeV2Length(uint8 biome) internal view returns (uint) { return _S().heroesByBiomeV2[biome].length(); } function heroesByBiomeV2ByIndex(uint8 biome, uint index) internal view returns (address helper, uint helperId) { bytes32 packedHelper =_S().heroesByBiomeV2[biome].at(index); (helper, helperId) = PackingLib.unpackNftId(packedHelper); } function heroesByBiomeV2(uint8 biome) internal view returns (address[] memory helpers, uint[] memory helperIds) { EnumerableSet.Bytes32Set storage packedHeroes = _S().heroesByBiomeV2[biome]; uint len = packedHeroes.length(); helpers = new address[](len); helperIds = new uint[](len); for (uint i; i < len; ++i) { (helpers[i], helperIds[i]) = PackingLib.unpackNftId(packedHeroes.at(i)); } return (helpers, helperIds); } //endregion ------------------------ VIEWS //region ------------------------ GOV ACTIONS function setConfigV2(IController controller, IReinforcementController.ConfigReinforcementV2 memory config) internal { onlyDeployer(controller); _S().configParams[IReinforcementController.ConfigParams.V2_MIN_MAX_BOARD_0] = uint( PackingLib.packConfigReinforcementV2(config.minNumberHits, config.maxNumberHits, config.lowDivider, config.highDivider, config.levelLimit) ); } //endregion ------------------------ GOV ACTIONS //region ------------------------ Reinforcement V1 /// @notice Reverse operation for {stakeHero} function withdrawHero(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal { IReinforcementController.MainState storage s = _S(); _checkWithdrawAllowed(isEoa, controller, msgSender, heroToken, heroId); (uint8 biome, , , ) = PackingLib.unpackReinforcementHeroInfo(s._stakedHeroes[heroToken.packNftId(heroId)]); if (biome == 0) revert IAppErrors.NotStaked(); s._internalIdsByBiomes[biome].remove(heroToken.packNftId(heroId)); delete s._stakedHeroes[heroToken.packNftId(heroId)]; emit IApplicationEvents.HeroWithdraw(heroToken, heroId); } function unpackHeroInfo(bytes32 packed) internal pure returns (IReinforcementController.HeroInfo memory info) { (info.biome, info.score, info.fee, info.stakeTs) = PackingLib.unpackReinforcementHeroInfo(packed); return info; } //endregion ------------------------ Reinforcement V1 //region ------------------------ Rewards for reinforcement of any kind /// @notice For classic reinforcement: register reward in _S(), keep tokens on balance of this contract /// For guild reinforcement: re-send reward to the guild bank. /// @dev Only for dungeon. Assume the tokens already sent to this contract. function registerTokenReward(IController controller, address heroToken, uint heroId, address token, uint amount) internal { onlyDungeonFactory(controller); uint guildId = busyGuildHelperOf(heroToken, heroId); if (guildId == 0) { // classic reinforcement: save all rewards to _heroTokenRewards EnumerableMap.AddressToUintMap storage rewards = _S()._heroTokenRewards[heroToken.packNftId(heroId)]; (,uint existAmount) = rewards.tryGet(token); rewards.set(token, existAmount + amount); emit IApplicationEvents.TokenRewardRegistered(heroToken, heroId, token, amount, existAmount + amount); } else { // guild reinforcement: send all rewards to guild bank // assume that guildController is initialized address guildBank = IGuildController(controller.guildController()).getGuildBank(guildId); IERC20(token).transfer(guildBank, amount); emit IApplicationEvents.GuildTokenRewardRegistered(heroToken, heroId, token, amount, guildId); } } /// @notice For classic reinforcement: register reward in _S(), keep the token on balance of this contract /// For guild reinforcement: re-send NFT-reward to the guild bank. /// @dev Only for dungeon. Assume the NFT already sent to this contract. function registerNftReward(IController controller, address heroToken, uint heroId, address token, uint tokenId) internal { onlyDungeonFactory(controller); uint guildId = busyGuildHelperOf(heroToken, heroId); if (guildId == 0) { // classic reinforcement: save all rewards to _heroNftRewards _S()._heroNftRewards[heroToken.packNftId(heroId)].push(token.packNftId(tokenId)); emit IApplicationEvents.NftRewardRegistered(heroToken, heroId, token, tokenId); } else { // guild reinforcement: send all rewards to guild bank // assume that guildController is initialized address guildBank = IGuildController(controller.guildController()).getGuildBank(guildId); IERC721(token).transferFrom(address(this), guildBank, tokenId); emit IApplicationEvents.GuildNftRewardRegistered(heroToken, heroId, token, tokenId, guildId); } } function claimAll(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal { onlyNotPausedEoaOwner(isEoa, controller, msgSender, heroToken, heroId); _claimAllTokenRewards(heroToken, heroId, msgSender); _claimAllNftRewards(heroToken, heroId, msgSender); } function claimNft( bool isEoa, IController controller, address msgSender, address heroToken, uint heroId, uint countNft ) internal { onlyNotPausedEoaOwner(isEoa, controller, msgSender, heroToken, heroId); _claimNftRewards(heroToken, heroId, msgSender, countNft); } //endregion ------------------------ Rewards for reinforcement of any kind //region ------------------------ Guild reinforcement function stakeGuildHero(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal { _checkStakeAllowed(isEoa, controller, msgSender, heroToken, heroId); IReinforcementController.MainState storage s = _S(); bytes32 packedHero = heroToken.packNftId(heroId); (, uint guildId) = _memberOf(controller, msgSender); uint lastGuildHeroWithdraw = s.lastGuildHeroWithdrawTs[packedHero]; if (block.timestamp - HERO_COOLDOWN_PERIOD_AFTER_GUILD_HERO_WITHDRAWING < lastGuildHeroWithdraw) revert IAppErrors.GuildReinforcementCooldownPeriod(); s.stakedGuildHeroes[packedHero] = guildId; // there is a chance that the hero is being used in reinforcement as result of previous staking uint busyByGuidId = s.busyGuildHelpers[packedHero]; s.guildHelpers[guildId].set(packedHero, busyByGuidId); emit IApplicationEvents.GuildHeroStaked(heroToken, heroId, guildId); } function withdrawGuildHero(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal { _checkWithdrawAllowed(isEoa, controller, msgSender, heroToken, heroId); IReinforcementController.MainState storage s = _S(); bytes32 packedHero = heroToken.packNftId(heroId); uint guildId = s.stakedGuildHeroes[packedHero]; if (guildId == 0) revert IAppErrors.NotStakedInGuild(); delete s.stakedGuildHeroes[packedHero]; if (s.guildHelpers[guildId].contains(packedHero)) { s.guildHelpers[guildId].remove(packedHero); } s.lastGuildHeroWithdrawTs[packedHero] = block.timestamp; emit IApplicationEvents.GuildHeroWithdrawn(heroToken, heroId, guildId); } /// @param hero Assume that the hero has no reinforcement, it's checked inside ItemController /// @param helper Desired helper. It should be staked by a member of the user's guild. function askGuildHero(IController controller, address hero, uint heroId, address helper, uint helperId) internal returns ( int32[] memory attributes ) { onlyHeroController(controller); address user = IERC721(hero).ownerOf(heroId); IReinforcementController.MainState storage s = _S(); (IGuildController gc, uint guildId) = _memberOf(controller, user); onlyGuildWithShelterEnoughLevel(gc, guildId); // ensure that the helper is free bytes32 packedHelper = PackingLib.packNftId(helper, helperId); EnumerableMap.Bytes32ToUintMap storage guildHelpers = s.guildHelpers[guildId]; if (!guildHelpers.contains(packedHelper)) revert IAppErrors.GuildHelperNotAvailable(guildId, helper, helperId); // mark the helper as busy guildHelpers.set(packedHelper, guildId); s.busyGuildHelpers[packedHelper] = guildId; attributes = _getReinforcementAttributes(controller, helper, helperId); emit IApplicationEvents.GuildHeroAsked(helper, helperId, guildId, user); return attributes; } function releaseGuildHero(IController controller, address helperHeroToken, uint helperHeroTokenId) internal { onlyHeroController(controller); bytes32 packedHero = helperHeroToken.packNftId(helperHeroTokenId); IReinforcementController.MainState storage s = _S(); // there is a chance that the helperId is already burnt // see test "use guild reinforcement - burn the helper-hero that is being used by reinforcement" // so, we cannot check HeroBase(helperHeroToken).ownerOf(helperHeroTokenId) here without try/catch address owner = getHeroTokenOwnerSafe(helperHeroToken, helperHeroTokenId); if (s.busyGuildHelpers[packedHero] == 0) revert IAppErrors.NotBusyGuildHelper(); uint guildIdStakedIn = s.stakedGuildHeroes[packedHero]; if (guildIdStakedIn != 0) { s.guildHelpers[guildIdStakedIn].set(packedHero, 0); // free for use in guild reinforcement again } s.busyGuildHelpers[packedHero] = 0; emit IApplicationEvents.GuildHeroReleased(helperHeroToken, helperHeroTokenId, guildIdStakedIn, owner); } //endregion ------------------------ Guild reinforcement //region ------------------------ Reinforcement V2 /// @notice Stake hero in reinforcement-v2 /// @param rewardAmount Reward required by the helper for the help. function stakeHeroV2(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId, uint rewardAmount) internal { IReinforcementController.MainState storage s = _S(); if (rewardAmount == 0) revert IAppErrors.ZeroAmount(); IHeroController heroController = _checkStakeAllowed(isEoa, controller, msgSender, heroToken, heroId); IStatController statController = IStatController(controller.statController()); uint8 biome = heroController.heroBiome(heroToken, heroId); { (, , , , uint8 levelLimit) = getConfigV2(); IStatController.ChangeableStats memory stats = statController.heroStats(heroToken, heroId); if (levelLimit != 0) { // levelLimit can be 0 in functional tests if (stats.level > levelLimit && (stats.level - levelLimit) / levelLimit > biome) revert IAppErrors.StakeHeroNotStats(); } if (stats.lifeChances == 0) revert IAppErrors.ErrorHeroIsDead(heroToken, heroId); } s.heroesByBiomeV2[biome].add(heroToken.packNftId(heroId)); s.stakedHeroesV2[heroToken.packNftId(heroId)] = IReinforcementController.HeroInfoV2({ biome: biome, stakeTs: uint64(block.timestamp), rewardAmount: uint128(rewardAmount) }); emit IApplicationEvents.HeroStakedV2(heroToken, heroId, biome, rewardAmount); } /// @notice Reverse operation for {stakeHeroV2} function withdrawHeroV2(bool isEoa, IController controller, address msgSender, address heroToken, uint heroId) internal { IReinforcementController.MainState storage s = _S(); _checkWithdrawAllowed(isEoa, controller, msgSender, heroToken, heroId); bytes32 packedHero = heroToken.packNftId(heroId); IReinforcementController.HeroInfoV2 memory _heroInfoV2 = s.stakedHeroesV2[packedHero]; if (_heroInfoV2.biome == 0) revert IAppErrors.NotStaked(); s.heroesByBiomeV2[_heroInfoV2.biome].remove(packedHero); delete s.stakedHeroesV2[packedHero]; emit IApplicationEvents.HeroWithdraw(heroToken, heroId); } /// @notice {hero} asks help of the {helper} /// Hero owner sends reward amount to the helper owner as the reward for the help. /// Hero owner sends fixed fee to controller using standard process-routine. /// Size of the fixed fee depends on total number of calls of {askHeroV2} for last 24 hours since the current moment. /// Durability of all equipped items of the helper are reduced. /// Assume, that hero owner approves rewardAmount + fixed fee to reinforcementController-contract /// - rewardAmount: amount required by the helper (see {heroInfoV2}) /// - fixed fee: fee taken by controller (see {getFeeAmount}) function askHeroV2(IController controller, address hero, uint heroId, address helper, uint helperId, uint blockTimestamp) internal returns ( int32[] memory attributes ) { // assume that the signer (HeroController) has checked that the hero and helper are registered, controller is not paused uint8 heroBiome; { address heroController = onlyHeroController(controller); heroBiome = IHeroController(heroController).heroBiome(hero, heroId); } address gameToken = controller.gameToken(); IReinforcementController.HeroInfoV2 memory _heroInfo = _S().stakedHeroesV2[helper.packNftId(helperId)]; if (_heroInfo.biome != heroBiome) revert IAppErrors.HelperNotAvailableInGivenBiome(); // calculate number of calls for the last 24 hours starting from the current moment uint hitsLast24h = _getHitsLast24h(heroBiome, blockTimestamp); // calculate fixed fee and send it to the treasury uint fixedFee = _getFeeAmount(gameToken, hitsLast24h, heroBiome); { address heroOwner = IERC721(hero).ownerOf(heroId); IERC20(gameToken).transferFrom(heroOwner, address(this), fixedFee + _heroInfo.rewardAmount); } { // send reward amount from msgSender to helper address helperOwner = IERC721(helper).ownerOf(helperId); IERC20(gameToken).transfer(helperOwner, _heroInfo.rewardAmount); } AppLib.approveIfNeeded(gameToken, fixedFee, address(controller)); controller.process(gameToken, fixedFee, address(this)); attributes = _getReinforcementAttributes(controller, helper, helperId); // reduceDurability of all equipped items of the helper IItemController(controller.itemController()).reduceDurability(helper, helperId, heroBiome, true); emit IApplicationEvents.HeroAskV2(helper, helperId, hitsLast24h, fixedFee, _heroInfo.rewardAmount); return attributes; } //endregion ------------------------ Reinforcement V2 //region ------------------------ Internal logic /// @notice Increment counter of hits, calculate actual number of hits for 24 hours starting from the current moment /// @return hitsLast24h Number of calls for the last 24 hours, decimals 18 function _getHitsLast24h(uint biome, uint blockTimestamp) internal returns (uint hitsLast24h) { IReinforcementController.LastWindowsV2 memory stat24h = _S().stat24hV2[biome]; (hitsLast24h, stat24h) = getHitsNumberPerLast24Hours(blockTimestamp, BASKET_INTERVAL, stat24h); _S().stat24hV2[biome] = stat24h; // save updated statistics for last 24 hours } function _getReinforcementAttributes(IController controller, address heroToken, uint heroTokenId) internal view returns ( int32[] memory attributes ) { IStatController sc = IStatController(controller.statController()); uint[] memory indexes = new uint[](12); indexes[0] = uint(IStatController.ATTRIBUTES.STRENGTH); indexes[1] = uint(IStatController.ATTRIBUTES.DEXTERITY); indexes[2] = uint(IStatController.ATTRIBUTES.VITALITY); indexes[3] = uint(IStatController.ATTRIBUTES.ENERGY); indexes[4] = uint(IStatController.ATTRIBUTES.DAMAGE_MIN); indexes[5] = uint(IStatController.ATTRIBUTES.DAMAGE_MAX); indexes[6] = uint(IStatController.ATTRIBUTES.ATTACK_RATING); indexes[7] = uint(IStatController.ATTRIBUTES.DEFENSE); indexes[8] = uint(IStatController.ATTRIBUTES.BLOCK_RATING); indexes[9] = uint(IStatController.ATTRIBUTES.FIRE_RESISTANCE); indexes[10] = uint(IStatController.ATTRIBUTES.COLD_RESISTANCE); indexes[11] = uint(IStatController.ATTRIBUTES.LIGHTNING_RESISTANCE); return _generateReinforcementAttributes(sc, indexes, heroToken, heroTokenId); } /// @notice Claim all rewards from {_heroTokenRewards} to {recipient}, remove data from {_heroTokenRewards} function _claimAllTokenRewards(address heroToken, uint heroId, address recipient) internal { EnumerableMap.AddressToUintMap storage rewards = _S()._heroTokenRewards[heroToken.packNftId(heroId)]; uint length = rewards.length(); address[] memory tokens = new address[](length); for (uint i; i < length; ++i) { (address token, uint amount) = rewards.at(i); IERC20(token).transfer(recipient, amount); emit IApplicationEvents.ClaimedToken(heroToken, heroId, token, amount, recipient); tokens[i] = token; } // need to remove after the ordered reading for handle all elements, just remove the struct will not work coz contains mapping inside for (uint i; i < length; ++i) { rewards.remove(tokens[i]); } } function _claimAllNftRewards(address heroToken, uint heroId, address recipient) internal { bytes32[] storage rewards = _S()._heroNftRewards[heroToken.packNftId(heroId)]; uint length = rewards.length; for (uint i; i < length; ++i) { (address token, uint id) = rewards[i].unpackNftId(); IERC721(token).safeTransferFrom(address(this), recipient, id); emit IApplicationEvents.ClaimedItem(heroToken, heroId, token, id, recipient); } // a simple array can be just deleted delete _S()._heroNftRewards[heroToken.packNftId(heroId)]; } /// @notice Claim last {countNft} NFTs and remove them from {_heroNftRewards} function _claimNftRewards(address heroToken, uint heroId, address recipient, uint countNft) internal { bytes32[] storage rewards = _S()._heroNftRewards[heroToken.packNftId(heroId)]; uint length = rewards.length; uint indexLastToDelete = countNft >= length ? 0 : length - countNft; while (length != indexLastToDelete) { (address token, uint id) = rewards[length - 1].unpackNftId(); IERC721(token).safeTransferFrom(address(this), recipient, id); emit IApplicationEvents.ClaimedItem(heroToken, heroId, token, id, recipient); length--; // if we are going to remove all items we can just delete all items at the end // otherwise we should pop the items one by one if (indexLastToDelete != 0) { rewards.pop(); } } if (length == 0) { delete _S()._heroNftRewards[heroToken.packNftId(heroId)]; } } function _generateReinforcementAttributes(IStatController sc, uint[] memory indexes, address heroToken, uint heroId) internal view returns (int32[] memory attributes) { attributes = new int32[](uint(IStatController.ATTRIBUTES.END_SLOT)); for (uint i; i < indexes.length; ++i) { attributes[indexes[i]] = CalcLib.max32(sc.heroAttribute(heroToken, heroId, indexes[i]) * _ATTRIBUTES_RATIO / 100, 1); } } //endregion ------------------------ Internal logic //region ------------------------ Fixed fee calculation V2 /// @notice hitsLast24h Number of hits per last 24 hours, decimals 18 function _getFeeAmount(address gameToken, uint hitsLast24h, uint8 biome) internal view returns (uint) { // get min-max range for the burn fee (uint32 minNumberHits, uint32 maxNumberHits, uint32 lowDivider, uint32 highDivider,) = getConfigV2(); uint min = 1e18 * uint(minNumberHits); uint max = 1e18 * uint(maxNumberHits); // get max amount of the fee using original minter.amountForDungeon // we should pass heroBiome EXACTLY same to dungeonBiomeLevel // to avoid reducing base because of the difference heroBiome and dungeonBiomeLevel, see {amountForDungeon} IMinter minter = IMinter(IGameToken(gameToken).minter()); uint amountForDungeon = minter.amountForDungeon(biome, biome) * 10; // calculate fee if (hitsLast24h < min) hitsLast24h = min; if (hitsLast24h > max) hitsLast24h = max; uint f = 1e18 * (hitsLast24h - min) / (max - min); return amountForDungeon / lowDivider + f * (amountForDungeon / highDivider - amountForDungeon / lowDivider) / 1e18; } /// @notice Process the next call of askHeroV2 /// @return hitsLast24h Number of askHeroV2-calls for last 24 hours, decimals 18 /// @return dest Updated last-24hours-window-statistics to be stored in the storage function getHitsNumberPerLast24Hours( uint blockTimestamp, uint basketInterval, IReinforcementController.LastWindowsV2 memory s ) internal pure returns ( uint hitsLast24h, IReinforcementController.LastWindowsV2 memory dest ) { uint countBaskets = 24 / basketInterval; uint hour = blockTimestamp / 60 / 60; // get absolute index of basket for the current hour uint targetBasketIndex = hour / basketInterval; if (s.basketIndex == targetBasketIndex) { // current basket is not changed, increase the counter if (s.basketValue < type(uint24).max) { s.basketValue++; } } else { // current basket is changed // save value of previous basket to {baskets} and start counting from the zero s.baskets[s.basketIndex % countBaskets] = s.basketValue; // clear outdated baskets if some baskets were skipped (users didn't make any actions too long) if (targetBasketIndex >= s.basketIndex + countBaskets) { for (uint i; i < countBaskets; ++i) { s.baskets[i] = 0; } } else { for (uint i = s.basketIndex + 1; i < targetBasketIndex; ++i) { s.baskets[i % countBaskets] = 0; } } s.basketValue = 1; s.basketIndex = uint48(targetBasketIndex); } // calculate sum for last 24 hours uint m = 1e18 * (blockTimestamp - targetBasketIndex * basketInterval * 60 * 60) / (basketInterval * 60 * 60); uint bi = s.basketIndex % countBaskets; for (uint i; i < countBaskets; ++i) { if (i == bi) { hitsLast24h += uint(s.baskets[i]) * (1e18 - m) + m * uint(s.basketValue); } else { hitsLast24h += uint(s.baskets[i]) * 1e18; } } return (hitsLast24h, s); } //endregion ------------------------ Fixed fee calculation V2 //region ------------------------ Common utils /// @return heroOwner Return token owner or zero address if the hero token is burnt function getHeroTokenOwnerSafe(address heroToken, uint heroTokenId) internal view returns (address heroOwner) { if (heroToken == address(0) || heroTokenId == 0) return address(0); try IERC721(heroToken).ownerOf(heroTokenId) returns (address owner) { // there is a chance that the hero token is already burnt // so, we cannot call {ownerOf} without try/catch heroOwner = owner; } catch {} return heroOwner; } //endregion ------------------------ Common utils }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IApplicationEvents.sol"; import "../interfaces/IERC20.sol"; import "../interfaces/IGameToken.sol"; import "../interfaces/IRewardsPool.sol"; import "../openzeppelin/Math.sol"; import "../proxy/Controllable.sol"; library RewardsPoolLib { /// @dev keccak256(abi.encode(uint256(keccak256("rewards.pool.main")) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant REWARDS_POOL_STORAGE_LOCATION = 0x6ad655e44097c54b487e7c9215cc0bbf37bbe7fc2f8034e2ddf6749036fda500; // rewards.pool.main //region ------------------------ Storage function _S() internal pure returns (IRewardsPool.MainState storage s) { assembly { s.slot := REWARDS_POOL_STORAGE_LOCATION } return s; } //endregion ------------------------ Storage //region ------------------------ Restrictions function onlyHeroController(IController controller) internal view { if (controller.heroController() != msg.sender) revert IAppErrors.ErrorNotHeroController(msg.sender); } function _onlyDeployer(IController controller) internal view { if (!controller.isDeployer(msg.sender)) revert IAppErrors.ErrorNotDeployer(msg.sender); } function _onlyGovernance(IController controller) internal view { if (controller.governance() != msg.sender) revert IAppErrors.NotGovernance(msg.sender); } //endregion ------------------------ Restrictions //region ------------------------ View function balanceOfToken(address token) internal view returns (uint) { return IERC20(token).balanceOf(address(this)); } function baseAmount(address token) internal view returns (uint) { return _S().baseAmounts[token]; } /// @param maxBiome Max available biome, see {IDungeonFactory.state.maxBiome} /// @param maxNgLevel Max opened NG_LEVEL, see {IHeroController.state.maxOpenedNgLevel} /// @param biome Current hero biome [0..19 /// @param heroNgLevel Current hero NG_LVL [0..99] /// @return Reward percent, decimals 18 function rewardPercent(uint maxBiome, uint maxNgLevel, uint biome, uint heroNgLevel) internal pure returns (uint) { // biome_sum = max biome*(max biome+1)/2 // biome_weight = biome / biome_sum // reward_percent = biome_weight * (1 + NG_LVL) / ng_sum return 1e18 * biome * (1 + heroNgLevel) / (maxBiome * (maxBiome + 1) / 2) // biome_sum / getNgSum(maxNgLevel); } /// @notice be definition ng_sum = (max_ng + 1) * (max_ng+2) / 2 function getNgSum(uint maxNgLevel) internal pure returns (uint) { return ((maxNgLevel + 1) * (maxNgLevel + 2) / 2); } function rewardAmount(address token, uint maxBiome, uint maxNgLevel, uint biome, uint heroNgLevel) internal view returns (uint) { return baseAmount(token) * rewardPercent(maxBiome, maxNgLevel, biome, heroNgLevel) / 1e18; } /// @notice Calculate lost profit amount in percents in the case when hero is created on {heroNgLevel} > 0 /// @param maxBiome Max available biome, see {IDungeonFactory.state.maxBiome} /// @param maxNgLevel Max opened NG_LEVEL, see {IHeroController.state.maxOpenedNgLevel} /// @param heroNgLevel NG_LVL [1..99] where the hero is created, assume heroNgLevel > 0 /// @return Lost reward percent, decimals 18 function lostProfitPercent(uint maxBiome, uint maxNgLevel, uint heroNgLevel) internal pure returns (uint) { uint percent; for (uint8 ngLevel = 0; ngLevel < heroNgLevel; ++ngLevel) { percent += totalProfitOnLevel(maxBiome, maxNgLevel, ngLevel); } return percent; } /// @notice SCR-1064: Calculate a percent to reduce drop chance of the monsters on various NG-levels. /// The percent is reverse to the percent of the rewards. /// @param maxBiome Max available biome, see {IDungeonFactory.state.maxBiome} /// @param maxNgLevel Max opened NG_LEVEL, see {IHeroController.state.maxOpenedNgLevel} /// @param heroNgLevel NG_LVL [1..99] where the hero is created, assume heroNgLevel > 0 /// @return Drop chance percent, decimals 18 function dropChancePercent(uint maxBiome, uint maxNgLevel, uint heroNgLevel) internal pure returns (uint) { if (heroNgLevel == 0) return 1e18; // NG0 is special case - drop is NOT reduced return heroNgLevel > maxNgLevel ? 0 : totalProfitOnLevel(maxBiome, maxNgLevel, maxNgLevel - heroNgLevel + 1); } /// @notice Calculate total percent of rewards in all biomes on the given {ngLevel} function totalProfitOnLevel(uint maxBiome, uint maxNgLevel, uint ngLevel) internal pure returns (uint percent) { for (uint8 biome = 1; biome <= maxBiome; ++biome) { percent += rewardPercent(maxBiome, maxNgLevel, biome, ngLevel); } return percent; } //endregion ------------------------ View //region ------------------------ Gov actions function setBaseAmount(IController controller, address token, uint baseAmount_) internal { _onlyDeployer(controller); emit IApplicationEvents.BaseAmountChanged(_S().baseAmounts[token], baseAmount_); _S().baseAmounts[token] = baseAmount_; } function withdraw(IController controller, address token, uint amount, address receiver) internal { _onlyGovernance(controller); IERC20(token).transfer(receiver, amount); } //endregion ------------------------ Gov actions //region ------------------------ Logic /// @notice Send {amount} of the {token} to the {dungeon} /// @dev Assume here that all calculations and checks are made on dungeonFactory-side function sendReward(IController controller, address token, uint rewardAmount_, address receiver) internal { onlyHeroController(controller); uint balance = IERC20(token).balanceOf(address(this)); if (balance >= rewardAmount_) { IERC20(token).transfer(receiver, rewardAmount_); emit IApplicationEvents.RewardSentToUser(receiver, token, rewardAmount_); } else { // there is not enough amount on reward pool balance // just register reward in events // assume that the reward should be paid to the receiver later manually emit IApplicationEvents.NotEnoughReward(receiver, token, rewardAmount_); } } //endregion ------------------------ Logic }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/Math.sol"; import "../interfaces/IStatController.sol"; import "./CalcLib.sol"; library ScoreLib { using CalcLib for int32; // core uint public constant STRENGTH = 100; uint public constant DEXTERITY = 100; uint public constant VITALITY = 100; uint public constant ENERGY = 100; // attributes uint public constant MELEE_DAMAGE = 10; uint public constant ATTACK_RATING = 3; uint public constant DEFENCE = 10; uint public constant BLOCK_RATING = 500; uint public constant LIFE = 10; uint public constant MANA = 10; uint public constant LIFE_CHANCES = 10_000; uint public constant MAGIC_FIND = 300; uint public constant CRITICAL_HIT = 150; uint public constant DMG_FACTOR = 200; uint public constant AR_FACTOR = 200; uint public constant LIFE_STOLEN_PER_HIT = 1000; uint public constant MANA_AFTER_KILL = 1000; uint public constant DAMAGE_REDUCTION = 500; uint public constant REFLECT_DAMAGE = 250; uint public constant RESIST_TO_STATUSES = 70; // resistance uint public constant ELEMENT_RESIST = 100; // race specific attributes uint public constant RACE_SPECIFIC = 20; // statuses uint public constant STATUSES = 100; // items uint public constant DURABILITY_SCORE = 1; // hero uint public constant HERO_LEVEL_SCORE = 1000; /// @param isForReinforcement If true calculate score using 12 main attributes only. Otherwise use all attributes. function attributesScore(int32[] memory attributes, bool isForReinforcement) internal pure returns (uint) { uint result; { result += (attributes[uint(IStatController.ATTRIBUTES.STRENGTH)]).toUint() * STRENGTH + (attributes[uint(IStatController.ATTRIBUTES.DEXTERITY)]).toUint() * DEXTERITY + (attributes[uint(IStatController.ATTRIBUTES.VITALITY)]).toUint() * VITALITY + (attributes[uint(IStatController.ATTRIBUTES.ENERGY)]).toUint() * ENERGY + (attributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)]).toUint() * ATTACK_RATING + (attributes[uint(IStatController.ATTRIBUTES.DEFENSE)]).toUint() * DEFENCE + (attributes[uint(IStatController.ATTRIBUTES.BLOCK_RATING)]).toUint() * BLOCK_RATING + Math.average(attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)].toUint(), attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)].toUint()) * MELEE_DAMAGE ; } { result += (attributes[uint(IStatController.ATTRIBUTES.FIRE_RESISTANCE)]).toUint() * ELEMENT_RESIST + (attributes[uint(IStatController.ATTRIBUTES.COLD_RESISTANCE)]).toUint() * ELEMENT_RESIST + (attributes[uint(IStatController.ATTRIBUTES.LIGHTNING_RESISTANCE)]).toUint() * ELEMENT_RESIST; } if (! isForReinforcement) { { result += (attributes[uint(IStatController.ATTRIBUTES.LIFE)]).toUint() * LIFE + (attributes[uint(IStatController.ATTRIBUTES.MANA)]).toUint() * MANA; } { result += (attributes[uint(IStatController.ATTRIBUTES.DMG_AGAINST_HUMAN)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DMG_AGAINST_UNDEAD)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DMG_AGAINST_DAEMON)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DMG_AGAINST_BEAST)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DEF_AGAINST_HUMAN)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DEF_AGAINST_UNDEAD)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DEF_AGAINST_DAEMON)]).toUint() * RACE_SPECIFIC + (attributes[uint(IStatController.ATTRIBUTES.DEF_AGAINST_BEAST)]).toUint() * RACE_SPECIFIC; } { result += (attributes[uint(IStatController.ATTRIBUTES.STUN)]).toUint() * STATUSES + (attributes[uint(IStatController.ATTRIBUTES.BURN)]).toUint() * STATUSES + (attributes[uint(IStatController.ATTRIBUTES.FREEZE)]).toUint() * STATUSES + (attributes[uint(IStatController.ATTRIBUTES.CONFUSE)]).toUint() * STATUSES + (attributes[uint(IStatController.ATTRIBUTES.CURSE)]).toUint() * STATUSES + (attributes[uint(IStatController.ATTRIBUTES.POISON)]).toUint() * STATUSES; } { result += (attributes[uint(IStatController.ATTRIBUTES.LIFE_CHANCES)]).toUint() * LIFE_CHANCES + (attributes[uint(IStatController.ATTRIBUTES.MAGIC_FIND)]).toUint() * MAGIC_FIND + (attributes[uint(IStatController.ATTRIBUTES.CRITICAL_HIT)]).toUint() * CRITICAL_HIT + (attributes[uint(IStatController.ATTRIBUTES.MELEE_DMG_FACTOR)]).toUint() * DMG_FACTOR + (attributes[uint(IStatController.ATTRIBUTES.FIRE_DMG_FACTOR)]).toUint() * DMG_FACTOR + (attributes[uint(IStatController.ATTRIBUTES.COLD_DMG_FACTOR)]).toUint() * DMG_FACTOR + (attributes[uint(IStatController.ATTRIBUTES.LIGHTNING_DMG_FACTOR)]).toUint() * DMG_FACTOR; } { result += (attributes[uint(IStatController.ATTRIBUTES.AR_FACTOR)]).toUint() * AR_FACTOR + (attributes[uint(IStatController.ATTRIBUTES.LIFE_STOLEN_PER_HIT)]).toUint() * LIFE_STOLEN_PER_HIT + (attributes[uint(IStatController.ATTRIBUTES.MANA_AFTER_KILL)]).toUint() * MANA_AFTER_KILL + (attributes[uint(IStatController.ATTRIBUTES.DAMAGE_REDUCTION)]).toUint() * DAMAGE_REDUCTION + (attributes[uint(IStatController.ATTRIBUTES.REFLECT_DAMAGE_MELEE)]).toUint() * REFLECT_DAMAGE + (attributes[uint(IStatController.ATTRIBUTES.REFLECT_DAMAGE_MAGIC)]).toUint() * REFLECT_DAMAGE + (attributes[uint(IStatController.ATTRIBUTES.RESIST_TO_STATUSES)]).toUint() * RESIST_TO_STATUSES; } } return result; } function itemScore(int32[] memory attributes, uint16 baseDurability) internal pure returns (uint) { return attributesScore(attributes, false) + baseDurability * DURABILITY_SCORE; } function heroScore(int32[] memory attributes, uint level) internal pure returns (uint) { return attributesScore(attributes, true) + level * HERO_LEVEL_SCORE; } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IApplicationEvents.sol"; import "../interfaces/IShelterController.sol"; import "../interfaces/IShelterController.sol"; import "../interfaces/IUserController.sol"; import "../lib/StringLib.sol"; import "../token/GuildBank.sol"; import "./StatLib.sol"; import "../interfaces/IShelterAuction.sol"; library ShelterLib { using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.UintSet; //region ------------------------ Constants /// @dev keccak256(abi.encode(uint256(keccak256("shelter.controller.main")) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant SHELTER_CONTROLLER_STORAGE_LOCATION = 0x5a293071b39954a4fcf98ae7184af7c6201e972e15842b884f1ad071e9bded00; // shelter.controller.main uint8 internal constant MIN_SHELTER_LEVEL = 1; uint8 internal constant MAX_SHELTER_LEVEL = 3; //endregion ------------------------ Constants //region ------------------------ Restrictions function _onlyDeployer(IController controller) internal view { if (!controller.isDeployer(msg.sender)) revert IAppErrors.ErrorNotDeployer(msg.sender); } function _onlyGuildController(address guildController) internal view { if (msg.sender != guildController) revert IAppErrors.ErrorNotGuildController(); } function _notPaused(IController controller) internal view { if (controller.onPause()) revert IAppErrors.ErrorPaused(); } //endregion ------------------------ Restrictions //region ------------------------ Storage function _S() internal pure returns (IShelterController.MainState storage s) { assembly { s.slot := SHELTER_CONTROLLER_STORAGE_LOCATION } return s; } //endregion ------------------------ Storage //region ------------------------ Shelter view /// @notice Get list of all registered shelters in the given {biome} function getShelters(uint8 biome) internal view returns (uint[] memory shelterIds) { return _S().shelters[biome].values(); } /// @notice Get initial price of the given shelter. The price is used if the shelter doesn't belong to any guild function getShelterPrice(uint shelterId) internal view returns (uint price) { return _S().shelterPrices[shelterId]; } /// @notice Get shelter which belongs to the given guild function guildToShelter(uint guildId) internal view returns (uint shelterId) { return _S().guildToShelter[guildId]; } /// @notice Get guild to which the given shelter belongs function shelterToGuild(uint shelterId) internal view returns (uint guildId) { return _S().shelterToGuild[shelterId]; } /// @notice Get shelter of the guild to which the user belongs function getShelterId(IGuildController guildController, address user) internal view returns (uint shelterId) { uint guildId = guildController.memberOf(user); return guildId == 0 ? 0 : _S().guildToShelter[guildId]; } /// @notice List of items that can be bought in the shelter of the given level in the given biome function getShelterItems(uint shelterId) internal view returns (address[] memory items) { return _S().shelterItems[shelterId].values(); } function getShelterItemData(uint shelterId, address item) internal view returns ( uint64 priceInPvpPoints, uint128 priceInGameToken, uint16 maxItemsPerDayLimit ) { IShelterController.ShelterItemData memory data = _S().shelterItemData[shelterId][item]; return ( data.priceInPvpPoints, data.priceInGameToken, data.maxItemsPerDayLimit ); } /// @notice How many {item} instances were purchased per {epochDay} in the given {shelterId} /// @param epochDay TimestampInSeconds / 24 * 60 * 60 function getCountPurchasedItems(address item, uint shelterId, uint32 epochDay) internal view returns (uint) { return _S().countPurchasedItems[shelterId][epochDay][item]; } //endregion ------------------------ Shelter view //region ------------------------ Shelter config /// @notice Register new shelter or overwrite exist. Only registered shelters can be purchased. /// @param shelterId ID should be generated using {PackingLib.packShelterId} /// @param price Initial shelter price in game tokens function setShelter(IController controller, uint shelterId, uint price) internal { ShelterLib._onlyDeployer(controller); (uint8 biome, uint8 shelterLevel, ) = PackingLib.unpackShelterId(shelterId); if (biome == 0 || biome > StatLib.MAX_POSSIBLE_BIOME) revert IAppErrors.ErrorIncorrectBiome(biome); if (price == 0) revert IAppErrors.ZeroValueNotAllowed(); if (shelterLevel < MIN_SHELTER_LEVEL || shelterLevel > MAX_SHELTER_LEVEL) revert IAppErrors.IncorrectShelterLevel(shelterLevel); _S().shelterPrices[shelterId] = price; _S().shelters[biome].add(shelterId); emit IApplicationEvents.RegisterShelter(shelterId, price); } /// @notice Set items that can be purchases in the given shelter: remove previously stored items, add new items. /// @param shelterId ID should be generated using {PackingLib.packShelterId} /// @param items List of item tokens /// @param pricesInPvpPoints Prices in pvp-points. The points are taken from guild balance at the moment of purchasing /// @param pricesInGameTokens Additional prices in game tokens. Can contain zeros. /// @param maxItemsPerDayLimits Indicate how many item instances the users can purchase per day. 0 - no limitations function setShelterItems( IController controller, uint shelterId, address[] memory items, uint64[] memory pricesInPvpPoints, uint128[] memory pricesInGameTokens, uint16[] memory maxItemsPerDayLimits ) internal { ShelterLib._onlyDeployer(controller); uint len = items.length; if (len != pricesInPvpPoints.length || len != pricesInGameTokens.length || len != maxItemsPerDayLimits.length) { revert IAppErrors.LengthsMismatch(); } EnumerableSet.AddressSet storage set = _S().shelterItems[shelterId]; // remove previously stored items address[] memory prevItems = set.values(); uint prevItemsLen = prevItems.length; for (uint i; i < prevItemsLen; ++i) { set.remove(prevItems[i]); delete _S().shelterItemData[shelterId][prevItems[i]]; } // add new items for (uint i; i < len; ++i) { set.add(items[i]); if (pricesInPvpPoints[i] == 0 && pricesInGameTokens[i] == 0) revert IAppErrors.FreeShelterItemsAreNotAllowed(shelterId, items[i]); _S().shelterItemData[shelterId][items[i]] = IShelterController.ShelterItemData({ priceInPvpPoints: pricesInPvpPoints[i], priceInGameToken: pricesInGameTokens[i], maxItemsPerDayLimit: maxItemsPerDayLimits[i] }); } emit IApplicationEvents.SetShelterItems(shelterId, items, pricesInPvpPoints, pricesInGameTokens, maxItemsPerDayLimits); } //endregion ------------------------ Shelter config //region ------------------------ Shelter actions /// @notice Guild buys a shelter that doesn't belong to any guild. It pays default prices and changes owner of the shelter. function buyShelter(IController controller, address msgSender, uint shelterId) internal { _notPaused(controller); IGuildController guildController = IGuildController(controller.guildController()); (uint guildId,) = guildController.checkPermissions(msgSender, uint(IGuildController.GuildRightBits.CHANGE_SHELTER_3)); // only registered shelter can be purchased (uint8 biome, , ) = PackingLib.unpackShelterId(shelterId); if (!_S().shelters[biome].contains(shelterId)) revert IAppErrors.ShelterIsNotRegistered(); // Each guild is able to have only 1 shelter. Exist shelter should be sold or left if (_S().guildToShelter[guildId] != 0) revert IAppErrors.GuildAlreadyHasShelter(); if (_S().shelterToGuild[shelterId] != 0) revert IAppErrors.ShelterIsBusy(); { // Shelter can be bought only if there is no auction bid address shelterAuction = guildController.shelterAuctionController(); if (shelterAuction != address(0)) { (uint positionId,) = IShelterAuction(shelterAuction).positionByBuyer(guildId); if (positionId != 0) revert IAppErrors.AuctionBidOpened(positionId); } } // pay for the shelter from the guild bank uint shelterPrice = getShelterPrice(shelterId); guildController.payFromGuildBank(guildId, shelterPrice); // register ownership _S().guildToShelter[guildId] = shelterId; _S().shelterToGuild[shelterId] = guildId; emit IApplicationEvents.BuyShelter(guildId, shelterId); } /// @notice Guild leaves the shelter. The shelter becomes free, it can be bought by any guild by default price function leaveShelter(IController controller, address msgSender, uint shelterId) internal { _notPaused(controller); IGuildController guildController = IGuildController(controller.guildController()); (uint guildId,) = guildController.checkPermissions(msgSender, uint(IGuildController.GuildRightBits.CHANGE_SHELTER_3)); if (_S().guildToShelter[guildId] != shelterId) revert IAppErrors.ShelterIsNotOwnedByTheGuild(); if (shelterId == 0) revert IAppErrors.GuildHasNoShelter(); { // Shelter can be sold only if there is no opened auction position address shelterAuction = guildController.shelterAuctionController(); if (shelterAuction != address(0)) { uint positionId = IShelterAuction(shelterAuction).positionBySeller(guildId); if (positionId != 0) revert IAppErrors.AuctionPositionOpened(positionId); } } // unregister ownership delete _S().guildToShelter[guildId]; delete _S().shelterToGuild[shelterId]; emit IApplicationEvents.LeaveShelter(guildId, shelterId); } /// @notice Purchase the {item} in the shelter that belongs to the guild to which {msgSender} belongs function purchaseShelterItem(IController controller, address msgSender, address item, uint blockTimestamp) internal { _notPaused(controller); IGuildController guildController = IGuildController(controller.guildController()); // no permission are required - any guild member is able to purchase shelter item // but the member should either be owner or should have enough pvp-points capacity, see restriction below uint guildId = _getValidGuildId(guildController, msgSender); uint shelterId = _S().guildToShelter[guildId]; if (shelterId == 0) revert IAppErrors.GuildHasNoShelter(); if (! _S().shelterItems[shelterId].contains(item)) revert IAppErrors.ShelterHasNotItem(shelterId, item); // total number of the item instances that can be minted per day CAN BE limited IShelterController.ShelterItemData memory itemData = _S().shelterItemData[shelterId][item]; uint numSoldItems; { uint32 epochDay = uint32(blockTimestamp / 86400); mapping(address => uint) storage countPurchasedItems = _S().countPurchasedItems[shelterId][epochDay]; numSoldItems = countPurchasedItems[item]; if (itemData.maxItemsPerDayLimit != 0) { if (numSoldItems >= itemData.maxItemsPerDayLimit) revert IAppErrors.MaxNumberItemsSoldToday(numSoldItems, itemData.maxItemsPerDayLimit); } countPurchasedItems[item] = numSoldItems + 1; } // user pays for the item by pvp-points and/or by game token (it depends on the item settings) if (itemData.priceInPvpPoints != 0) { guildController.usePvpPoints(guildId, msgSender, itemData.priceInPvpPoints); } if (itemData.priceInGameToken != 0) { guildController.payFromBalance(itemData.priceInGameToken, msgSender); //_process(controller, itemData.priceInGameToken, msgSender); } // mint the item IItemController(controller.itemController()).mint(item, msgSender); emit IApplicationEvents.PurchaseShelterItem(msgSender, item, numSoldItems + 1, itemData.priceInPvpPoints, itemData.priceInGameToken); } /// @notice clear necessary data to indicate that the guiles leaves the shelter function clearShelter(address guildController, uint guildId) internal { _onlyGuildController(guildController); uint shelterId = _S().guildToShelter[guildId]; if (shelterId != 0) { // assume, that msgSender shouldn't have permission CHANGE_SHELTER_3 here // ensure that there is no open position for the shelter on auction address shelterAuction = IGuildController(guildController).shelterAuctionController(); if (shelterAuction != address(0)) { uint positionId = IShelterAuction(shelterAuction).positionBySeller(guildId); if (positionId != 0) revert IAppErrors.AuctionPositionOpened(positionId); } delete _S().guildToShelter[guildId]; delete _S().shelterToGuild[shelterId]; emit IApplicationEvents.LeaveShelter(guildId, shelterId); } } //endregion ------------------------ Shelter actions //region ------------------------ Interaction with auctions function changeShelterOwner(IController controller, uint shelterId, uint newOwnerGuildId) internal { // we assume, that all checks are performed on ShelterAuction side, so we need min checks here address shelterAuction = IGuildController(controller.guildController()).shelterAuctionController(); if (shelterAuction == address(0) || msg.sender != shelterAuction) revert IAppErrors.NotShelterAuction(); uint prevGuildId = _S().shelterToGuild[shelterId]; delete _S().guildToShelter[prevGuildId]; _S().shelterToGuild[shelterId] = newOwnerGuildId; _S().guildToShelter[newOwnerGuildId] = shelterId; emit IApplicationEvents.ChangeShelterOwner(shelterId, prevGuildId, newOwnerGuildId); } //endregion ------------------------ Interaction with auctions //region ------------------------ Internal logic function _getValidGuildId(IGuildController guildController, address user) internal view returns (uint guildId) { guildId = guildController.memberOf(user); if (guildId == 0) revert IAppErrors.NotGuildMember(); } //endregion ------------------------ Internal logic }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; /// @title Library for setting / getting slot variables (used in upgradable proxy contracts) /// @author bogdoslav library SlotsLib { /// @notice Version of the contract /// @dev Should be incremented when contract changed string public constant SLOT_LIB_VERSION = "1.0.0"; // ************* GETTERS ******************* /// @dev Gets a slot as bytes32 function getBytes32(bytes32 slot) internal view returns (bytes32 result) { assembly { result := sload(slot) } } /// @dev Gets a slot as an address function getAddress(bytes32 slot) internal view returns (address result) { assembly { result := sload(slot) } } /// @dev Gets a slot as uint256 function getUint(bytes32 slot) internal view returns (uint result) { assembly { result := sload(slot) } } // ************* ARRAY GETTERS ******************* /// @dev Gets an array length function arrayLength(bytes32 slot) internal view returns (uint result) { assembly { result := sload(slot) } } /// @dev Gets a slot array by index as address /// @notice First slot is array length, elements ordered backward in memory /// @notice This is unsafe, without checking array length. function addressAt(bytes32 slot, uint index) internal view returns (address result) { bytes32 pointer = bytes32(uint(slot) - 1 - index); assembly { result := sload(pointer) } } // ************* SETTERS ******************* /// @dev Sets a slot with bytes32 /// @notice Check address for 0 at the setter function set(bytes32 slot, bytes32 value) internal { assembly { sstore(slot, value) } } /// @dev Sets a slot with address /// @notice Check address for 0 at the setter function set(bytes32 slot, address value) internal { assembly { sstore(slot, value) } } /// @dev Sets a slot with uint function set(bytes32 slot, uint value) internal { assembly { sstore(slot, value) } } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../interfaces/IStatController.sol"; import "../interfaces/IHeroController.sol"; import "../interfaces/IAppErrors.sol"; import "../openzeppelin/Math.sol"; import "./CalcLib.sol"; import "./PackingLib.sol"; library StatLib { using PackingLib for bytes32[]; using PackingLib for bytes32; using PackingLib for uint32[]; using PackingLib for int32[]; using CalcLib for int32; //region --------------------------- Constants /// @notice Version of the contract /// @dev Should be incremented when contract changed string public constant STAT_LIB_VERSION = "1.0.0"; uint32 public constant MAX_LEVEL = 99; uint public constant BASE_EXPERIENCE = 100_000; uint public constant BIOME_LEVEL_STEP = 5; uint internal constant _MAX_AMPLIFIER = 1e18; uint private constant _PRECISION = 1e18; uint private constant VIRTUAL_LEVEL_GAP = 2; /// @dev Assume MAX_BIOME * BIOME_LEVEL_STEP < MAX_LEVEL + 1, see dungeonTreasuryReward uint public constant MAX_POSSIBLE_BIOME = 19; //endregion --------------------------- Constants //region --------------------------- Data types struct BaseMultiplier { uint minDamage; uint maxDamage; uint attackRating; uint defense; uint blockRating; uint life; uint mana; } struct LevelUp { uint life; uint mana; } struct InitialHero { IStatController.CoreAttributes core; BaseMultiplier multiplier; LevelUp levelUp; int32 baseLifeChances; } enum HeroClasses { UNKNOWN, THRALL, SAVAGE, MAGE, ASSASSIN, GHOST, HAMMERGINA, END_SLOT } //endregion --------------------------- Data types //region --------------------------- BASE function isNetworkWithOldSavage() public view returns (bool) { return block.chainid == uint(111188) || block.chainid == uint(250); } // --- HERO 1 (Slave) --- function initialHero1() internal pure returns (InitialHero memory) { return InitialHero({ core: IStatController.CoreAttributes({ strength: 15, dexterity: 15, vitality: 30, energy: 10 }), multiplier: BaseMultiplier({ minDamage: 0.1e18, maxDamage: 0.2e18, attackRating: 2e18, defense: 2e18, blockRating: 0.1e18, life: 1.5e18, mana: 0.5e18 }), levelUp: LevelUp({ life: 2e18, mana: 1e18 }), baseLifeChances: 5 }); } // --- HERO 2 (Spata) --- function initialHero2() internal view returns (InitialHero memory) { bool old = isNetworkWithOldSavage(); return InitialHero({ core: IStatController.CoreAttributes({ strength: 30, dexterity: 5, vitality: 25, energy: 10 }), multiplier: BaseMultiplier({ minDamage: 0.15e18, maxDamage: old ? 0.25e18 : 0.5e18, attackRating: old ? 2e18 : 3e18, defense: 1e18, blockRating: 0.08e18, life: 1.3e18, mana: 0.5e18 }), levelUp: LevelUp({ life: 1.8e18, mana: 1e18 }), baseLifeChances: 5 }); } // --- HERO 3 (Decidia) --- function initialHero3() internal pure returns (InitialHero memory) { return InitialHero({ core: IStatController.CoreAttributes({ strength: 10, dexterity: 15, vitality: 20, energy: 25 }), multiplier: BaseMultiplier({ minDamage: 0.1e18, maxDamage: 0.2e18, attackRating: 2e18, defense: 1e18, blockRating: 0.1e18, life: 1e18, mana: 2e18 }), levelUp: LevelUp({ life: 1.3e18, mana: 2e18 }), baseLifeChances: 5 }); } // --- HERO 4 (Innatus) --- function initialHero4() internal pure returns (InitialHero memory) { return InitialHero({ core: IStatController.CoreAttributes({ strength: 15, dexterity: 25, vitality: 15, energy: 15 }), multiplier: BaseMultiplier({ minDamage: 0.1e18, maxDamage: 0.2e18, attackRating: 4e18, defense: 3e18, blockRating: 0.2e18, life: 1.2e18, mana: 1e18 }), levelUp: LevelUp({ life: 1.7e18, mana: 1.5e18 }), baseLifeChances: 5 }); } // --- HERO 5 (F2P) --- function initialHero5() internal pure returns (InitialHero memory) { return InitialHero({ core: IStatController.CoreAttributes({ strength: 20, dexterity: 20, vitality: 20, energy: 10 }), multiplier: BaseMultiplier({ minDamage: 0.15e18, maxDamage: 0.25e18, attackRating: 3e18, defense: 2.5e18, blockRating: 0.15e18, life: 1.5e18, mana: 1.5e18 }), levelUp: LevelUp({ life: 1.5e18, mana: 1.5e18 }), baseLifeChances: 1 }); } // --- HERO 6 (F2P) HAMMERGINA --- function initialHero6() internal pure returns (InitialHero memory) { return InitialHero({ core: IStatController.CoreAttributes({ strength: 50, dexterity: 30, vitality: 50, energy: 15 }), multiplier: BaseMultiplier({ minDamage: 0.2e18, maxDamage: 0.3e18, attackRating: 5e18, defense: 3e18, blockRating: 0.15e18, life: 2e18, mana: 2e18 }), levelUp: LevelUp({ life: 1.7e18, mana: 1.5e18 }), baseLifeChances: 1 }); } // ------ function initialHero(uint heroClass) internal view returns (InitialHero memory) { if (heroClass == 1) { return initialHero1(); } else if (heroClass == 2) { return initialHero2(); } else if (heroClass == 3) { return initialHero3(); } else if (heroClass == 4) { return initialHero4(); } else if (heroClass == 5) { return initialHero5(); } else if (heroClass == 6) { return initialHero6(); } else { revert IAppErrors.UnknownHeroClass(heroClass); } } //endregion --------------------------- BASE //region --------------------------- CALCULATIONS function minDamage(int32 strength, uint heroClass) internal view returns (int32) { return int32(int(strength.toUint() * initialHero(heroClass).multiplier.minDamage / _PRECISION)); } function maxDamage(int32 strength, uint heroClass) internal view returns (int32){ return int32(int(strength.toUint() * initialHero(heroClass).multiplier.maxDamage / _PRECISION)); } function attackRating(int32 dexterity, uint heroClass) internal view returns (int32){ return int32(int(dexterity.toUint() * initialHero(heroClass).multiplier.attackRating / _PRECISION)); } function defense(int32 dexterity, uint heroClass) internal view returns (int32){ return int32(int(dexterity.toUint() * initialHero(heroClass).multiplier.defense / _PRECISION)); } function blockRating(int32 dexterity, uint heroClass) internal view returns (int32){ return int32(int(Math.min((dexterity.toUint() * initialHero(heroClass).multiplier.blockRating / _PRECISION), 75))); } function life(int32 vitality, uint heroClass, uint32 level) internal view returns (int32){ return int32(int( (vitality.toUint() * initialHero(heroClass).multiplier.life / _PRECISION) + (uint(level) * initialHero(heroClass).levelUp.life / _PRECISION) )); } function mana(int32 energy, uint heroClass, uint32 level) internal view returns (int32){ return int32(int( (energy.toUint() * initialHero(heroClass).multiplier.mana / _PRECISION) + (uint(level) * initialHero(heroClass).levelUp.mana / _PRECISION) )); } function lifeChances(uint heroClass, uint32 /*level*/) internal view returns (int32){ return initialHero(heroClass).baseLifeChances; } function levelExperience(uint32 level) internal pure returns (uint32) { if (level == 0 || level >= MAX_LEVEL) { return 0; } return uint32(uint(level) * BASE_EXPERIENCE * (67e17 - CalcLib.log2((uint(MAX_LEVEL - level + 2)) * 1e18)) / 1e18); } function chanceToHit( uint attackersAttackRating, uint defendersDefenceRating, uint attackersLevel, uint defendersLevel, uint arFactor ) internal pure returns (uint) { attackersAttackRating += attackersAttackRating * arFactor / 100; uint x = Math.max(attackersAttackRating, 1); uint y = Math.max(attackersAttackRating + defendersDefenceRating, 1); uint z = attackersLevel; uint k = defendersLevel / 2; uint xy = x * 1e18 / y; uint zk = z * 1e18 / (attackersLevel + k); uint base = 2 * xy * zk / 1e18; return Math.max(Math.min(base, 0.95e18), 0.2e18); } function experienceToVirtualLevel(uint experience, uint startFromLevel) internal pure returns (uint level) { level = startFromLevel; for (; level < MAX_LEVEL;) { if (levelExperience(uint32(level)) >= (experience + 1)) { break; } unchecked{++level;} } } function expPerMonster(uint32 monsterExp, uint monsterRarity, uint32 /*heroExp*/, uint32 /*heroCurrentLvl*/, uint /*monsterBiome*/) internal pure returns (uint32) { // do not reduce exp per level, it is no economical sense return uint32(uint(monsterExp) + uint(monsterExp) * monsterRarity / _MAX_AMPLIFIER); } /// @notice Allow to calculate delta param for {mintDropChance} function mintDropChanceDelta(uint heroCurrentExp, uint heroCurrentLevel, uint monsterBiome) internal pure returns (uint) { uint heroBiome = getVirtualLevel(heroCurrentExp, heroCurrentLevel, true) / StatLib.BIOME_LEVEL_STEP + 1; return heroBiome > monsterBiome ? 2 ** (heroBiome - monsterBiome + 10) : 0; } function getVirtualLevel(uint heroCurrentExp, uint heroCurrentLevel, bool withGap) internal pure returns (uint) { uint virtualLevel = StatLib.experienceToVirtualLevel(heroCurrentExp, heroCurrentLevel); if (withGap && (virtualLevel + 1) > VIRTUAL_LEVEL_GAP) { virtualLevel -= VIRTUAL_LEVEL_GAP; } return virtualLevel; } function initAttributes( bytes32[] storage attributes, uint heroClass, uint32 level, IStatController.CoreAttributes memory base ) internal returns (uint32[] memory result) { attributes.setInt32(uint(IStatController.ATTRIBUTES.STRENGTH), base.strength); attributes.setInt32(uint(IStatController.ATTRIBUTES.DEXTERITY), base.dexterity); attributes.setInt32(uint(IStatController.ATTRIBUTES.VITALITY), base.vitality); attributes.setInt32(uint(IStatController.ATTRIBUTES.ENERGY), base.energy); attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN), minDamage(base.strength, heroClass)); attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX), maxDamage(base.strength, heroClass)); attributes.setInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING), attackRating(base.dexterity, heroClass)); attributes.setInt32(uint(IStatController.ATTRIBUTES.DEFENSE), defense(base.dexterity, heroClass)); attributes.setInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING), blockRating(base.dexterity, heroClass)); attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE), life(base.vitality, heroClass, level)); attributes.setInt32(uint(IStatController.ATTRIBUTES.MANA), mana(base.energy, heroClass, level)); attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE_CHANCES), lifeChances(heroClass, level)); result = new uint32[](3); result[0] = uint32(life(base.vitality, heroClass, level).toUint()); result[1] = uint32(mana(base.energy, heroClass, level).toUint()); result[2] = uint32(lifeChances(heroClass, uint32(level)).toUint()); } function updateCoreDependAttributesInMemory( int32[] memory attributes, int32[] memory bonus, uint heroClass, uint32 level ) internal view returns (int32[] memory) { int32 strength = attributes[uint(IStatController.ATTRIBUTES.STRENGTH)]; int32 dexterity = attributes[uint(IStatController.ATTRIBUTES.DEXTERITY)]; int32 vitality = attributes[uint(IStatController.ATTRIBUTES.VITALITY)]; int32 energy = attributes[uint(IStatController.ATTRIBUTES.ENERGY)]; attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)] = minDamage(strength, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)]; attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)] = maxDamage(strength, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)]; attributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] = attackRating(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.ATTACK_RATING)]; attributes[uint(IStatController.ATTRIBUTES.DEFENSE)] = defense(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DEFENSE)]; attributes[uint(IStatController.ATTRIBUTES.BLOCK_RATING)] = blockRating(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.BLOCK_RATING)]; attributes[uint(IStatController.ATTRIBUTES.LIFE)] = life(vitality, heroClass, level) + bonus[uint(IStatController.ATTRIBUTES.LIFE)]; attributes[uint(IStatController.ATTRIBUTES.MANA)] = mana(energy, heroClass, level) + bonus[uint(IStatController.ATTRIBUTES.MANA)]; return attributes; } function updateCoreDependAttributes( bytes32[] storage attributes, bytes32[] storage bonusMain, bytes32[] storage bonusExtra, IStatController.ChangeableStats memory _heroStats, uint index, uint heroClass, int32 base ) internal { if (index == uint(IStatController.ATTRIBUTES.STRENGTH)) { attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN), StatLib.minDamage(base, heroClass) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN)) ); attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX), StatLib.maxDamage(base, heroClass) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX)) ); } else if (index == uint(IStatController.ATTRIBUTES.DEXTERITY)) { attributes.setInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING), StatLib.attackRating(base, heroClass) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING)) ); attributes.setInt32(uint(IStatController.ATTRIBUTES.DEFENSE), StatLib.defense(base, heroClass) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DEFENSE)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DEFENSE)) ); attributes.setInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING), StatLib.blockRating(base, heroClass) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING)) ); } else if (index == uint(IStatController.ATTRIBUTES.VITALITY)) { attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE), StatLib.life(base, heroClass, _heroStats.level) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.LIFE)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.LIFE)) ); } else if (index == uint(IStatController.ATTRIBUTES.ENERGY)) { attributes.setInt32(uint(IStatController.ATTRIBUTES.MANA), StatLib.mana(base, heroClass, _heroStats.level) + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.MANA)) + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.MANA)) ); } } function attributesAdd(int32[] memory base, int32[] memory add) internal pure returns (int32[] memory) { unchecked{ for (uint i; i < base.length; ++i) { base[i] += add[i]; } } return base; } // Currently this function is not used // function attributesRemove(int32[] memory base, int32[] memory remove) internal pure returns (int32[] memory) { // unchecked{ // for (uint i; i < base.length; ++i) { // base[i] = CalcLib.minusWithMinFloorI32(base[i], remove[i]); // } // } // return base; // } function packChangeableStats(IStatController.ChangeableStats memory stats) internal pure returns (bytes32) { uint32[] memory cData = new uint32[](5); cData[0] = stats.level; cData[1] = stats.experience; cData[2] = stats.life; cData[3] = stats.mana; cData[4] = stats.lifeChances; return cData.packUint32Array(); } function unpackChangeableStats(bytes32 data) internal pure returns (IStatController.ChangeableStats memory result) { uint32[] memory cData = data.unpackUint32Array(); return IStatController.ChangeableStats({ level: cData[0], experience: cData[1], life: cData[2], mana: cData[3], lifeChances: cData[4] }); } function bytesToFullAttributesArray(bytes32[] memory attributes) internal pure returns (int32[] memory result) { (int32[] memory values, uint8[] memory ids) = attributes.toInt32ArrayWithIds(); return valuesToFullAttributesArray(values, ids); } function valuesToFullAttributesArray(int32[] memory values, uint8[] memory ids) internal pure returns (int32[] memory result) { result = new int32[](uint(IStatController.ATTRIBUTES.END_SLOT)); for (uint i; i < values.length; ++i) { int32 value = values[i]; if (value != 0) { result[ids[i]] = value; } } } //endregion --------------------------- CALCULATIONS }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; library StringLib { /// @dev Inspired by OraclizeAPI's implementation - MIT license /// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol function toString(uint value) external pure returns (string memory) { return _toString(value); } function _toString(uint value) internal pure returns (string memory) { if (value == 0) { return "0"; } uint temp = value; uint digits; while (temp != 0) { digits++; temp /= 10; } bytes memory buffer = new bytes(digits); while (value != 0) { digits -= 1; buffer[digits] = bytes1(uint8(48 + uint(value % 10))); value /= 10; } return string(buffer); } function toAsciiString(address x) external pure returns (string memory) { return _toAsciiString(x); } function _toAsciiString(address x) internal pure returns (string memory) { bytes memory s = new bytes(40); for (uint i = 0; i < 20; i++) { bytes1 b = bytes1(uint8(uint(uint160(x)) / (2 ** (8 * (19 - i))))); bytes1 hi = bytes1(uint8(b) / 16); bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); s[2 * i] = _char(hi); s[2 * i + 1] = _char(lo); } return string(s); } function char(bytes1 b) external pure returns (bytes1 c) { return _char(b); } function _char(bytes1 b) internal pure returns (bytes1 c) { if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); else return bytes1(uint8(b) + 0x57); } function concat(string memory a, string memory b) internal pure returns (string memory) { return string(abi.encodePacked(a, b)); } function isASCIILettersOnly(string memory str) internal pure returns (bool) { bytes memory b = bytes(str); for (uint i = 0; i < b.length; i++) { if (uint8(b[i]) < 32 || uint8(b[i]) > 127) { return false; } } return true; } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. pragma solidity ^0.8.20; import {EnumerableSet} from "./EnumerableSet.sol"; /** * @dev Library for managing an enumerable variant of Solidity's * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] * type. * * Maps have the following properties: * * - Entries are added, removed, and checked for existence in constant time * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. * * ```solidity * contract Example { * // Add the library methods * using EnumerableMap for EnumerableMap.UintToAddressMap; * * // Declare a set state variable * EnumerableMap.UintToAddressMap private myMap; * } * ``` * * The following map types are supported: * * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0 * - `address -> uint256` (`AddressToUintMap`) since v4.6.0 * - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0 * - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0 * - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0 * * [WARNING] * ==== * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure * unusable. * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. * * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an * array of EnumerableMap. * ==== */ library EnumerableMap { using EnumerableSet for EnumerableSet.Bytes32Set; // To implement this library for multiple types with as little code repetition as possible, we write it in // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions, // and user-facing implementations such as `UintToAddressMap` are just wrappers around the underlying Map. // This means that we can only create new EnumerableMaps for types that fit in bytes32. /** * @dev Query for a nonexistent map key. */ error EnumerableMapNonexistentKey(bytes32 key); struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; mapping(bytes32 key => bytes32) _values; } /** * @dev Adds a key-value pair to a map, or updates the value for an existing * key. O(1). * * Returns true if the key was added to the map, that is if it was not * already present. */ function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) { map._values[key] = value; return map._keys.add(key); } /** * @dev Removes a key-value pair from a map. O(1). * * Returns true if the key was removed from the map, that is if it was present. */ function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) { delete map._values[key]; return map._keys.remove(key); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) { return map._keys.contains(key); } /** * @dev Returns the number of key-value pairs in the map. O(1). */ function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) { return map._keys.length(); } /** * @dev Returns the key-value pair stored at position `index` in the map. O(1). * * Note that there are no guarantees on the ordering of entries inside the * array, and it may change when more entries are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { bytes32 key = map._keys.at(index); return (key, map._values[key]); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { bytes32 value = map._values[key]; if (value == bytes32(0)) { return (contains(map, key), bytes32(0)); } else { return (true, value); } } /** * @dev Returns the value associated with `key`. O(1). * * Requirements: * * - `key` must be in the map. */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; if (value == 0 && !contains(map, key)) { revert EnumerableMapNonexistentKey(key); } return value; } /** * @dev Return the an array containing all the keys * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) { return map._keys.values(); } // UintToUintMap struct UintToUintMap { Bytes32ToBytes32Map _inner; } /** * @dev Adds a key-value pair to a map, or updates the value for an existing * key. O(1). * * Returns true if the key was added to the map, that is if it was not * already present. */ function set(UintToUintMap storage map, uint256 key, uint256 value) internal returns (bool) { return set(map._inner, bytes32(key), bytes32(value)); } /** * @dev Removes a value from a map. O(1). * * Returns true if the key was removed from the map, that is if it was present. */ function remove(UintToUintMap storage map, uint256 key) internal returns (bool) { return remove(map._inner, bytes32(key)); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(UintToUintMap storage map, uint256 key) internal view returns (bool) { return contains(map._inner, bytes32(key)); } /** * @dev Returns the number of elements in the map. O(1). */ function length(UintToUintMap storage map) internal view returns (uint256) { return length(map._inner); } /** * @dev Returns the element stored at position `index` in the map. O(1). * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(UintToUintMap storage map, uint256 index) internal view returns (uint256, uint256) { (bytes32 key, bytes32 value) = at(map._inner, index); return (uint256(key), uint256(value)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) { (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); return (success, uint256(value)); } /** * @dev Returns the value associated with `key`. O(1). * * Requirements: * * - `key` must be in the map. */ function get(UintToUintMap storage map, uint256 key) internal view returns (uint256) { return uint256(get(map._inner, bytes32(key))); } /** * @dev Return the an array containing all the keys * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ function keys(UintToUintMap storage map) internal view returns (uint256[] memory) { bytes32[] memory store = keys(map._inner); uint256[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // UintToAddressMap struct UintToAddressMap { Bytes32ToBytes32Map _inner; } /** * @dev Adds a key-value pair to a map, or updates the value for an existing * key. O(1). * * Returns true if the key was added to the map, that is if it was not * already present. */ function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) { return set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); } /** * @dev Removes a value from a map. O(1). * * Returns true if the key was removed from the map, that is if it was present. */ function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) { return remove(map._inner, bytes32(key)); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) { return contains(map._inner, bytes32(key)); } /** * @dev Returns the number of elements in the map. O(1). */ function length(UintToAddressMap storage map) internal view returns (uint256) { return length(map._inner); } /** * @dev Returns the element stored at position `index` in the map. O(1). * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { (bytes32 key, bytes32 value) = at(map._inner, index); return (uint256(key), address(uint160(uint256(value)))); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); return (success, address(uint160(uint256(value)))); } /** * @dev Returns the value associated with `key`. O(1). * * Requirements: * * - `key` must be in the map. */ function get(UintToAddressMap storage map, uint256 key) internal view returns (address) { return address(uint160(uint256(get(map._inner, bytes32(key))))); } /** * @dev Return the an array containing all the keys * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ function keys(UintToAddressMap storage map) internal view returns (uint256[] memory) { bytes32[] memory store = keys(map._inner); uint256[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // AddressToUintMap struct AddressToUintMap { Bytes32ToBytes32Map _inner; } /** * @dev Adds a key-value pair to a map, or updates the value for an existing * key. O(1). * * Returns true if the key was added to the map, that is if it was not * already present. */ function set(AddressToUintMap storage map, address key, uint256 value) internal returns (bool) { return set(map._inner, bytes32(uint256(uint160(key))), bytes32(value)); } /** * @dev Removes a value from a map. O(1). * * Returns true if the key was removed from the map, that is if it was present. */ function remove(AddressToUintMap storage map, address key) internal returns (bool) { return remove(map._inner, bytes32(uint256(uint160(key)))); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(AddressToUintMap storage map, address key) internal view returns (bool) { return contains(map._inner, bytes32(uint256(uint160(key)))); } /** * @dev Returns the number of elements in the map. O(1). */ function length(AddressToUintMap storage map) internal view returns (uint256) { return length(map._inner); } /** * @dev Returns the element stored at position `index` in the map. O(1). * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) { (bytes32 key, bytes32 value) = at(map._inner, index); return (address(uint160(uint256(key))), uint256(value)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); return (success, uint256(value)); } /** * @dev Returns the value associated with `key`. O(1). * * Requirements: * * - `key` must be in the map. */ function get(AddressToUintMap storage map, address key) internal view returns (uint256) { return uint256(get(map._inner, bytes32(uint256(uint160(key))))); } /** * @dev Return the an array containing all the keys * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ function keys(AddressToUintMap storage map) internal view returns (address[] memory) { bytes32[] memory store = keys(map._inner); address[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // Bytes32ToUintMap struct Bytes32ToUintMap { Bytes32ToBytes32Map _inner; } /** * @dev Adds a key-value pair to a map, or updates the value for an existing * key. O(1). * * Returns true if the key was added to the map, that is if it was not * already present. */ function set(Bytes32ToUintMap storage map, bytes32 key, uint256 value) internal returns (bool) { return set(map._inner, key, bytes32(value)); } /** * @dev Removes a value from a map. O(1). * * Returns true if the key was removed from the map, that is if it was present. */ function remove(Bytes32ToUintMap storage map, bytes32 key) internal returns (bool) { return remove(map._inner, key); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool) { return contains(map._inner, key); } /** * @dev Returns the number of elements in the map. O(1). */ function length(Bytes32ToUintMap storage map) internal view returns (uint256) { return length(map._inner); } /** * @dev Returns the element stored at position `index` in the map. O(1). * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32, uint256) { (bytes32 key, bytes32 value) = at(map._inner, index); return (key, uint256(value)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) { (bool success, bytes32 value) = tryGet(map._inner, key); return (success, uint256(value)); } /** * @dev Returns the value associated with `key`. O(1). * * Requirements: * * - `key` must be in the map. */ function get(Bytes32ToUintMap storage map, bytes32 key) internal view returns (uint256) { return uint256(get(map._inner, key)); } /** * @dev Return the an array containing all the keys * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ function keys(Bytes32ToUintMap storage map) internal view returns (bytes32[] memory) { bytes32[] memory store = keys(map._inner); bytes32[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. pragma solidity ^0.8.20; /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive * types. * * Sets have the following properties: * * - Elements are added, removed, and checked for existence in constant time * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * * ```solidity * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; * * // Declare a set state variable * EnumerableSet.AddressSet private mySet; * } * ``` * * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) * and `uint256` (`UintSet`) are supported. * * [WARNING] * ==== * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure * unusable. * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. * * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an * array of EnumerableSet. * ==== */ library EnumerableSet { // To implement this library for multiple types with as little code // repetition as possible, we write it in terms of a generic Set type with // bytes32 values. // The Set implementation uses private functions, and user-facing // implementations (such as AddressSet) are just wrappers around the // underlying Set. // This means that we can only create new EnumerableSets for types that fit // in bytes32. struct Set { // Storage of set values bytes32[] _values; // Position is the index of the value in the `values` array plus 1. // Position 0 is used to mean a value is not in the set. mapping(bytes32 value => uint256) _positions; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function _add(Set storage set, bytes32 value) private returns (bool) { if (!_contains(set, value)) { set._values.push(value); // The value is stored at length-1, but we add 1 to all indexes // and use 0 as a sentinel value set._positions[value] = set._values.length; return true; } else { return false; } } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function _remove(Set storage set, bytes32 value) private returns (bool) { // We cache the value's position to prevent multiple reads from the same storage slot uint256 position = set._positions[value]; if (position != 0) { // Equivalent to contains(set, value) // To delete an element from the _values array in O(1), we swap the element to delete with the last one in // the array, and then remove the last element (sometimes called as 'swap and pop'). // This modifies the order of the array, as noted in {at}. uint256 valueIndex = position - 1; uint256 lastIndex = set._values.length - 1; if (valueIndex != lastIndex) { bytes32 lastValue = set._values[lastIndex]; // Move the lastValue to the index where the value to delete is set._values[valueIndex] = lastValue; // Update the tracked position of the lastValue (that was just moved) set._positions[lastValue] = position; } // Delete the slot where the moved value was stored set._values.pop(); // Delete the tracked position for the deleted slot delete set._positions[value]; return true; } else { return false; } } /** * @dev Returns true if the value is in the set. O(1). */ function _contains(Set storage set, bytes32 value) private view returns (bool) { return set._positions[value] != 0; } /** * @dev Returns the number of values on the set. O(1). */ function _length(Set storage set) private view returns (uint256) { return set._values.length; } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function _at(Set storage set, uint256 index) private view returns (bytes32) { return set._values[index]; } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function _values(Set storage set) private view returns (bytes32[] memory) { return set._values; } // Bytes32Set struct Bytes32Set { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _add(set._inner, value); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { return _remove(set._inner, value); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { return _contains(set._inner, value); } /** * @dev Returns the number of values in the set. O(1). */ function length(Bytes32Set storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { return _at(set._inner, index); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { bytes32[] memory store = _values(set._inner); bytes32[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // AddressSet struct AddressSet { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(AddressSet storage set, address value) internal returns (bool) { return _add(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(AddressSet storage set, address value) internal returns (bool) { return _remove(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(AddressSet storage set, address value) internal view returns (bool) { return _contains(set._inner, bytes32(uint256(uint160(value)))); } /** * @dev Returns the number of values in the set. O(1). */ function length(AddressSet storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(AddressSet storage set, uint256 index) internal view returns (address) { return address(uint160(uint256(_at(set._inner, index)))); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(AddressSet storage set) internal view returns (address[] memory) { bytes32[] memory store = _values(set._inner); address[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } // UintSet struct UintSet { Set _inner; } /** * @dev Add a value to a set. O(1). * * Returns true if the value was added to the set, that is if it was not * already present. */ function add(UintSet storage set, uint256 value) internal returns (bool) { return _add(set._inner, bytes32(value)); } /** * @dev Removes a value from a set. O(1). * * Returns true if the value was removed from the set, that is if it was * present. */ function remove(UintSet storage set, uint256 value) internal returns (bool) { return _remove(set._inner, bytes32(value)); } /** * @dev Returns true if the value is in the set. O(1). */ function contains(UintSet storage set, uint256 value) internal view returns (bool) { return _contains(set._inner, bytes32(value)); } /** * @dev Returns the number of values in the set. O(1). */ function length(UintSet storage set) internal view returns (uint256) { return _length(set._inner); } /** * @dev Returns the value stored at position `index` in the set. O(1). * * Note that there are no guarantees on the ordering of values inside the * array, and it may change when more values are added or removed. * * Requirements: * * - `index` must be strictly less than {length}. */ function at(UintSet storage set, uint256 index) internal view returns (uint256) { return uint256(_at(set._inner, index)); } /** * @dev Return the entire set in an array * * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(UintSet storage set) internal view returns (uint256[] memory) { bytes32[] memory store = _values(set._inner); uint256[] memory result; /// @solidity memory-safe-assembly assembly { result := store } return result; } }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../interfaces/IERC721Receiver.sol"; /** * @dev Implementation of the {IERC721Receiver} interface. * * Accepts all token transfers. * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. */ contract ERC721Holder is IERC721Receiver { /** * @dev See {IERC721Receiver-onERC721Received}. * * Always returns `IERC721Receiver.onERC721Received.selector`. */ function onERC721Received( address, address, uint256, bytes memory ) public virtual override returns (bytes4) { return this.onERC721Received.selector; } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) pragma solidity ^0.8.20; /** * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. * * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in * case an upgrade adds a module that needs to be initialized. * * For example: * * [.hljs-theme-light.nopadding] * ```solidity * contract MyToken is ERC20Upgradeable { * function initialize() initializer public { * __ERC20_init("MyToken", "MTK"); * } * } * * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { * function initializeV2() reinitializer(2) public { * __ERC20Permit_init("MyToken"); * } * } * ``` * * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. * * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. * * [CAUTION] * ==== * Avoid leaving a contract uninitialized. * * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: * * [.hljs-theme-light.nopadding] * ``` * /// @custom:oz-upgrades-unsafe-allow constructor * constructor() { * _disableInitializers(); * } * ``` * ==== */ abstract contract Initializable { /** * @dev Storage of the initializable contract. * * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions * when using with upgradeable contracts. * * @custom:storage-location erc7201:openzeppelin.storage.Initializable */ struct InitializableStorage { /** * @dev Indicates that the contract has been initialized. */ uint64 _initialized; /** * @dev Indicates that the contract is in the process of being initialized. */ bool _initializing; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; /** * @dev The contract is already initialized. */ error InvalidInitialization(); /** * @dev The contract is not initializing. */ error NotInitializing(); /** * @dev Triggered when the contract has been initialized or reinitialized. */ event Initialized(uint64 version); /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. * * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in * production. * * Emits an {Initialized} event. */ modifier initializer() { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); // Cache values to avoid duplicated sloads bool isTopLevelCall = !$._initializing; uint64 initialized = $._initialized; // Allowed calls: // - initialSetup: the contract is not in the initializing state and no previous version was // initialized // - construction: the contract is initialized at version 1 (no reininitialization) and the // current contract is just being deployed bool initialSetup = initialized == 0 && isTopLevelCall; bool construction = initialized == 1 && address(this).code.length == 0; if (!initialSetup && !construction) { revert InvalidInitialization(); } $._initialized = 1; if (isTopLevelCall) { $._initializing = true; } _; if (isTopLevelCall) { $._initializing = false; emit Initialized(1); } } /** * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be * used to initialize parent contracts. * * A reinitializer may be used after the original initialization step. This is essential to configure modules that * are added through upgrades and that require initialization. * * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` * cannot be nested. If one is invoked in the context of another, execution will revert. * * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in * a contract, executing them in the right order is up to the developer or operator. * * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. * * Emits an {Initialized} event. */ modifier reinitializer(uint64 version) { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing || $._initialized >= version) { revert InvalidInitialization(); } $._initialized = version; $._initializing = true; _; $._initializing = false; emit Initialized(version); } /** * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { _checkInitializing(); _; } /** * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. */ function _checkInitializing() internal view virtual { if (!_isInitializing()) { revert NotInitializing(); } } /** * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized * to any version. It is recommended to use this to lock implementation contracts that are designed to be called * through proxies. * * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing) { revert InvalidInitialization(); } if ($._initialized != type(uint64).max) { $._initialized = type(uint64).max; emit Initialized(type(uint64).max); } } /** * @dev Returns the highest version that has been initialized. See {reinitializer}. */ function _getInitializedVersion() internal view returns (uint64) { return _getInitializableStorage()._initialized; } /** * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. */ function _isInitializing() internal view returns (bool) { return _getInitializableStorage()._initializing; } /** * @dev Returns a pointer to the storage namespace. */ // solhint-disable-next-line var-name-mixedcase function _getInitializableStorage() private pure returns (InitializableStorage storage $) { assembly { $.slot := INITIALIZABLE_STORAGE } } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol) pragma solidity ^0.8.20; /** * @dev Standard math utilities missing in the Solidity language. */ library Math { /** * @dev Muldiv operation overflow. */ error MathOverflowedMulDiv(); enum Rounding { Floor, // Toward negative infinity Ceil, // Toward positive infinity Trunc, // Toward zero Expand // Away from zero } /** * @dev Returns the addition of two unsigned integers, with an success flag (no overflow). */ function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { uint256 c = a + b; if (c < a) return (false, 0); return (true, c); } } /** * @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow). */ function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b > a) return (false, 0); return (true, a - b); } } /** * @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow). */ function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 if (a == 0) return (true, 0); uint256 c = a * b; if (c / a != b) return (false, 0); return (true, c); } } /** * @dev Returns the division of two unsigned integers, with a success flag (no division by zero). */ function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b == 0) return (false, 0); return (true, a / b); } } /** * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero). */ function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b == 0) return (false, 0); return (true, a % b); } } /** * @dev Returns the largest of two numbers. */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a : b; } /** * @dev Returns the smallest of two numbers. */ function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } /** * @dev Returns the average of two numbers. The result is rounded towards * zero. */ function average(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b) / 2 can overflow. return (a & b) + (a ^ b) / 2; } /** * @dev Returns the ceiling of the division of two numbers. * * This differs from standard division with `/` in that it rounds towards infinity instead * of rounding towards zero. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { if (b == 0) { // Guarantee the same behavior as in a regular Solidity division. return a / b; } // The following calculation ensures accurate ceiling division without overflow. // Since a is non-zero, (a - 1) / b will not overflow. // The largest possible result occurs when (a - 1) / b is type(uint256).max, // but the largest value we can obtain is type(uint256).max - 1, which happens // when a = type(uint256).max and b = 1. unchecked { return a == 0 ? 0 : (a - 1) / b + 1; } } /** * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or * denominator == 0. * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by * Uniswap Labs also under MIT license. */ function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { unchecked { // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 // variables such that product = prod1 * 2^256 + prod0. uint256 prod0 = x * y; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(x, y, not(0)) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { // Solidity will revert if denominator == 0, unlike the div opcode on its own. // The surrounding unchecked block does not change this fact. // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. return prod0 / denominator; } // Make sure the result is less than 2^256. Also prevents denominator == 0. if (denominator <= prod1) { revert MathOverflowedMulDiv(); } /////////////////////////////////////////////// // 512 by 256 division. /////////////////////////////////////////////// // Make division exact by subtracting the remainder from [prod1 prod0]. uint256 remainder; assembly { // Compute remainder using mulmod. remainder := mulmod(x, y, denominator) // Subtract 256 bit number from 512 bit number. prod1 := sub(prod1, gt(remainder, prod0)) prod0 := sub(prod0, remainder) } // Factor powers of two out of denominator and compute largest power of two divisor of denominator. // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. uint256 twos = denominator & (0 - denominator); assembly { // Divide denominator by twos. denominator := div(denominator, twos) // Divide [prod1 prod0] by twos. prod0 := div(prod0, twos) // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. twos := add(div(sub(0, twos), twos), 1) } // Shift in bits from prod1 into prod0. prod0 |= prod1 * twos; // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for // four bits. That is, denominator * inv = 1 mod 2^4. uint256 inverse = (3 * denominator) ^ 2; // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also // works in modular arithmetic, doubling the correct bits in each step. inverse *= 2 - denominator * inverse; // inverse mod 2^8 inverse *= 2 - denominator * inverse; // inverse mod 2^16 inverse *= 2 - denominator * inverse; // inverse mod 2^32 inverse *= 2 - denominator * inverse; // inverse mod 2^64 inverse *= 2 - denominator * inverse; // inverse mod 2^128 inverse *= 2 - denominator * inverse; // inverse mod 2^256 // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 // is no longer required. result = prod0 * inverse; return result; } } /** * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. */ function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { uint256 result = mulDiv(x, y, denominator); if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) { result += 1; } return result; } /** * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded * towards zero. * * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). */ function sqrt(uint256 a) internal pure returns (uint256) { if (a == 0) { return 0; } // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. // // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. // // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` // // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. uint256 result = 1 << (log2(a) >> 1); // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision // into the expected uint128 result. unchecked { result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; return min(result, a / result); } } /** * @notice Calculates sqrt(a), following the selected rounding direction. */ function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = sqrt(a); return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0); } } /** * @dev Return the log in base 2 of a positive value rounded towards zero. * Returns 0 if given 0. */ function log2(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 128; } if (value >> 64 > 0) { value >>= 64; result += 64; } if (value >> 32 > 0) { value >>= 32; result += 32; } if (value >> 16 > 0) { value >>= 16; result += 16; } if (value >> 8 > 0) { value >>= 8; result += 8; } if (value >> 4 > 0) { value >>= 4; result += 4; } if (value >> 2 > 0) { value >>= 2; result += 2; } if (value >> 1 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 2, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log2(value); return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0); } } /** * @dev Return the log in base 10 of a positive value rounded towards zero. * Returns 0 if given 0. */ function log10(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >= 10 ** 64) { value /= 10 ** 64; result += 64; } if (value >= 10 ** 32) { value /= 10 ** 32; result += 32; } if (value >= 10 ** 16) { value /= 10 ** 16; result += 16; } if (value >= 10 ** 8) { value /= 10 ** 8; result += 8; } if (value >= 10 ** 4) { value /= 10 ** 4; result += 4; } if (value >= 10 ** 2) { value /= 10 ** 2; result += 2; } if (value >= 10 ** 1) { result += 1; } } return result; } /** * @dev Return the log in base 10, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log10(value); return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0); } } /** * @dev Return the log in base 256 of a positive value rounded towards zero. * Returns 0 if given 0. * * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. */ function log256(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 16; } if (value >> 64 > 0) { value >>= 64; result += 8; } if (value >> 32 > 0) { value >>= 32; result += 4; } if (value >> 16 > 0) { value >>= 16; result += 2; } if (value >> 8 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 256, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log256(value); return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0); } } /** * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. */ function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { return uint8(rounding) % 2 == 1; } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; import "../openzeppelin/Initializable.sol"; import "../interfaces/IControllable.sol"; import "../interfaces/IController.sol"; import "../lib/SlotsLib.sol"; /// @title Implement basic functionality for any contract that require strict control /// @dev Can be used with upgradeable pattern. /// Require call __Controllable_init() in any case. /// @author belbix abstract contract Controllable is Initializable, IControllable { using SlotsLib for bytes32; /// @notice Version of the contract /// @dev Should be incremented when contract changed string public constant CONTROLLABLE_VERSION = "1.0.0"; bytes32 internal constant _CONTROLLER_SLOT = bytes32(uint256(keccak256("eip1967.controllable.controller")) - 1); bytes32 internal constant _CREATED_SLOT = bytes32(uint256(keccak256("eip1967.controllable.created")) - 1); bytes32 internal constant _CREATED_BLOCK_SLOT = bytes32(uint256(keccak256("eip1967.controllable.created_block")) - 1); bytes32 internal constant _REVISION_SLOT = bytes32(uint256(keccak256("eip1967.controllable.revision")) - 1); bytes32 internal constant _PREVIOUS_LOGIC_SLOT = bytes32(uint256(keccak256("eip1967.controllable.prev_logic")) - 1); event ContractInitialized(address controller, uint ts, uint block); event RevisionIncreased(uint value, address oldLogic); // init implementation contract constructor() initializer {} /// @notice Initialize contract after setup it as proxy implementation /// Save block.timestamp in the "created" variable /// @dev Use it only once after first logic setup /// @param controller_ Controller address function __Controllable_init(address controller_) internal onlyInitializing { _init(controller_); } function _init(address controller_) private { require(controller_ != address(0), "Zero controller"); _CONTROLLER_SLOT.set(controller_); _CREATED_SLOT.set(block.timestamp); _CREATED_BLOCK_SLOT.set(block.number); emit ContractInitialized(controller_, block.timestamp, block.number); } /// @dev Return true if given address is controller function isController(address value_) public override view returns (bool) { return value_ == controller(); } /// @notice Return true if given address is setup as governance in Controller function isGovernance(address value_) public override view returns (bool) { return IController(controller()).governance() == value_; } /// @dev Contract upgrade counter function revision() external view override returns (uint) { return _REVISION_SLOT.getUint(); } /// @dev Previous logic implementation function previousImplementation() external view override returns (address) { return _PREVIOUS_LOGIC_SLOT.getAddress(); } // ************* SETTERS/GETTERS ******************* /// @notice Return controller address saved in the contract slot function controller() public view override returns (address) { return _CONTROLLER_SLOT.getAddress(); } /// @notice Return creation timestamp /// @return Creation timestamp function created() external view override returns (uint256) { return _CREATED_SLOT.getUint(); } /// @notice Return creation block number /// @return Creation block number function createdBlock() external override view returns (uint256) { return _CREATED_BLOCK_SLOT.getUint(); } /// @dev Revision should be increased on each contract upgrade function increaseRevision(address oldLogic) external override { require(msg.sender == address(this), "Increase revision forbidden"); uint r = _REVISION_SLOT.getUint() + 1; _REVISION_SLOT.set(r); _PREVIOUS_LOGIC_SLOT.set(oldLogic); emit RevisionIncreased(r, oldLogic); } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol) pragma solidity ^0.8.1; import "../interfaces/IAppErrors.sol"; /** * @dev Context variant with ERC2771 support. */ // based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol abstract contract ERC2771Context { // for whitelist new relayers need to add new constants and update proxies address private constant GELATO_RELAY_1_BALANCE_ERC_2771 = 0xd8253782c45a12053594b9deB72d8e8aB2Fca54c; address private constant SACRA_RELAY = 0x52CEba41Da235Af367bFC0b0cCd3314cb901bB5F; address private constant SACRA_RELAY_2 = 0x102f1f556cD9C3D5f820E6920A8931657c5Da21B; function isTrustedForwarder(address forwarder) public view virtual returns (bool){ return forwarder == GELATO_RELAY_1_BALANCE_ERC_2771 || forwarder == SACRA_RELAY || forwarder == SACRA_RELAY_2; } function _msgSender() internal view virtual returns (address sender) { if (isTrustedForwarder(msg.sender)) { // The assembly code is more direct than the Solidity version using `abi.decode`. /// @solidity memory-safe-assembly assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } return sender; } else { return msg.sender; } } function _msgData() internal view virtual returns (bytes calldata) { if (isTrustedForwarder(msg.sender)) { return msg.data[: msg.data.length - 20]; } else { return msg.data; } } /// @notice Return true if given address is not a smart contract but a wallet address. /// @dev It is not 100% guarantee after EIP-3074 implementation, use it as an additional check. /// @return true if the address is a wallet. function _isNotSmartContract() internal view returns (bool) { return isTrustedForwarder(msg.sender) || msg.sender == tx.origin; } function onlyEOA() internal view { if (!_isNotSmartContract()) { revert IAppErrors.NotEOA(msg.sender); } } }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; /// @notice Arithmetic library with operations for fixed-point numbers. /// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol) /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) library FixedPointMathLib { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CUSTOM ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev The operation failed, as the output exceeds the maximum value of uint256. error ExpOverflow(); /// @dev The operation failed, as the output exceeds the maximum value of uint256. error FactorialOverflow(); /// @dev The operation failed, due to an overflow. error RPowOverflow(); /// @dev The mantissa is too big to fit. error MantissaOverflow(); /// @dev The operation failed, due to an multiplication overflow. error MulWadFailed(); /// @dev The operation failed, due to an multiplication overflow. error SMulWadFailed(); /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. error DivWadFailed(); /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. error SDivWadFailed(); /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. error MulDivFailed(); /// @dev The division failed, as the denominator is zero. error DivFailed(); /// @dev The full precision multiply-divide operation failed, either due /// to the result being larger than 256 bits, or a division by a zero. error FullMulDivFailed(); /// @dev The output is undefined, as the input is less-than-or-equal to zero. error LnWadUndefined(); /// @dev The input outside the acceptable domain. error OutOfDomain(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev The scalar of ETH and most ERC20s. uint256 internal constant WAD = 1e18; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* SIMPLIFIED FIXED POINT OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Equivalent to `(x * y) / WAD` rounded down. function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. if mul(y, gt(x, div(not(0), y))) { mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. revert(0x1c, 0x04) } z := div(mul(x, y), WAD) } } /// @dev Equivalent to `(x * y) / WAD` rounded down. function sMulWad(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := mul(x, y) // Equivalent to `require((x == 0 || z / x == y) && !(x == -1 && y == type(int256).min))`. if iszero(gt(or(iszero(x), eq(sdiv(z, x), y)), lt(not(x), eq(y, shl(255, 1))))) { mstore(0x00, 0xedcd4dd4) // `SMulWadFailed()`. revert(0x1c, 0x04) } z := sdiv(z, WAD) } } /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks. function rawMulWad(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := div(mul(x, y), WAD) } } /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks. function rawSMulWad(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := sdiv(mul(x, y), WAD) } } /// @dev Equivalent to `(x * y) / WAD` rounded up. function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. if mul(y, gt(x, div(not(0), y))) { mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. revert(0x1c, 0x04) } z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)) } } /// @dev Equivalent to `(x * y) / WAD` rounded up, but without overflow checks. function rawMulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)) } } /// @dev Equivalent to `(x * WAD) / y` rounded down. function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`. if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) { mstore(0x00, 0x7c5f487d) // `DivWadFailed()`. revert(0x1c, 0x04) } z := div(mul(x, WAD), y) } } /// @dev Equivalent to `(x * WAD) / y` rounded down. function sDivWad(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := mul(x, WAD) // Equivalent to `require(y != 0 && ((x * WAD) / WAD == x))`. if iszero(and(iszero(iszero(y)), eq(sdiv(z, WAD), x))) { mstore(0x00, 0x5c43740d) // `SDivWadFailed()`. revert(0x1c, 0x04) } z := sdiv(mul(x, WAD), y) } } /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks. function rawDivWad(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := div(mul(x, WAD), y) } } /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks. function rawSDivWad(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := sdiv(mul(x, WAD), y) } } /// @dev Equivalent to `(x * WAD) / y` rounded up. function divWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`. if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) { mstore(0x00, 0x7c5f487d) // `DivWadFailed()`. revert(0x1c, 0x04) } z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y)) } } /// @dev Equivalent to `(x * WAD) / y` rounded up, but without overflow and divide by zero checks. function rawDivWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y)) } } /// @dev Equivalent to `x` to the power of `y`. /// because `x ** y = (e ** ln(x)) ** y = e ** (ln(x) * y)`. function powWad(int256 x, int256 y) internal pure returns (int256) { // Using `ln(x)` means `x` must be greater than 0. return expWad((lnWad(x) * y) / int256(WAD)); } /// @dev Returns `exp(x)`, denominated in `WAD`. /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln function expWad(int256 x) internal pure returns (int256 r) { unchecked { // When the result is less than 0.5 we return zero. // This happens when `x <= (log(1e-18) * 1e18) ~ -4.15e19`. if (x <= -41446531673892822313) return r; /// @solidity memory-safe-assembly assembly { // When the result is greater than `(2**255 - 1) / 1e18` we can not represent it as // an int. This happens when `x >= floor(log((2**255 - 1) / 1e18) * 1e18) ≈ 135`. if iszero(slt(x, 135305999368893231589)) { mstore(0x00, 0xa37bfec9) // `ExpOverflow()`. revert(0x1c, 0x04) } } // `x` is now in the range `(-42, 136) * 1e18`. Convert to `(-42, 136) * 2**96` // for more intermediate precision and a binary basis. This base conversion // is a multiplication by 1e18 / 2**96 = 5**18 / 2**78. x = (x << 78) / 5 ** 18; // Reduce range of x to (-½ ln 2, ½ ln 2) * 2**96 by factoring out powers // of two such that exp(x) = exp(x') * 2**k, where k is an integer. // Solving this gives k = round(x / log(2)) and x' = x - k * log(2). int256 k = ((x << 96) / 54916777467707473351141471128 + 2 ** 95) >> 96; x = x - k * 54916777467707473351141471128; // `k` is in the range `[-61, 195]`. // Evaluate using a (6, 7)-term rational approximation. // `p` is made monic, we'll multiply by a scale factor later. int256 y = x + 1346386616545796478920950773328; y = ((y * x) >> 96) + 57155421227552351082224309758442; int256 p = y + x - 94201549194550492254356042504812; p = ((p * y) >> 96) + 28719021644029726153956944680412240; p = p * x + (4385272521454847904659076985693276 << 96); // We leave `p` in `2**192` basis so we don't need to scale it back up for the division. int256 q = x - 2855989394907223263936484059900; q = ((q * x) >> 96) + 50020603652535783019961831881945; q = ((q * x) >> 96) - 533845033583426703283633433725380; q = ((q * x) >> 96) + 3604857256930695427073651918091429; q = ((q * x) >> 96) - 14423608567350463180887372962807573; q = ((q * x) >> 96) + 26449188498355588339934803723976023; /// @solidity memory-safe-assembly assembly { // Div in assembly because solidity adds a zero check despite the unchecked. // The q polynomial won't have zeros in the domain as all its roots are complex. // No scaling is necessary because p is already `2**96` too large. r := sdiv(p, q) } // r should be in the range `(0.09, 0.25) * 2**96`. // We now need to multiply r by: // - The scale factor `s ≈ 6.031367120`. // - The `2**k` factor from the range reduction. // - The `1e18 / 2**96` factor for base conversion. // We do this all at once, with an intermediate result in `2**213` // basis, so the final right shift is always by a positive amount. r = int256( (uint256(r) * 3822833074963236453042738258902158003155416615667) >> uint256(195 - k) ); } } /// @dev Returns `ln(x)`, denominated in `WAD`. /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln function lnWad(int256 x) internal pure returns (int256 r) { /// @solidity memory-safe-assembly assembly { // We want to convert `x` from `10**18` fixed point to `2**96` fixed point. // We do this by multiplying by `2**96 / 10**18`. But since // `ln(x * C) = ln(x) + ln(C)`, we can simply do nothing here // and add `ln(2**96 / 10**18)` at the end. // Compute `k = log2(x) - 96`, `r = 159 - k = 255 - log2(x) = 255 ^ log2(x)`. r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) r := or(r, shl(4, lt(0xffff, shr(r, x)))) r := or(r, shl(3, lt(0xff, shr(r, x)))) // We place the check here for more optimal stack operations. if iszero(sgt(x, 0)) { mstore(0x00, 0x1615e638) // `LnWadUndefined()`. revert(0x1c, 0x04) } // forgefmt: disable-next-item r := xor(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), 0xf8f9f9faf9fdfafbf9fdfcfdfafbfcfef9fafdfafcfcfbfefafafcfbffffffff)) // Reduce range of x to (1, 2) * 2**96 // ln(2^k * x) = k * ln(2) + ln(x) x := shr(159, shl(r, x)) // Evaluate using a (8, 8)-term rational approximation. // `p` is made monic, we will multiply by a scale factor later. // forgefmt: disable-next-item let p := sub( // This heavily nested expression is to avoid stack-too-deep for via-ir. sar(96, mul(add(43456485725739037958740375743393, sar(96, mul(add(24828157081833163892658089445524, sar(96, mul(add(3273285459638523848632254066296, x), x))), x))), x)), 11111509109440967052023855526967) p := sub(sar(96, mul(p, x)), 45023709667254063763336534515857) p := sub(sar(96, mul(p, x)), 14706773417378608786704636184526) p := sub(mul(p, x), shl(96, 795164235651350426258249787498)) // We leave `p` in `2**192` basis so we don't need to scale it back up for the division. // `q` is monic by convention. let q := add(5573035233440673466300451813936, x) q := add(71694874799317883764090561454958, sar(96, mul(x, q))) q := add(283447036172924575727196451306956, sar(96, mul(x, q))) q := add(401686690394027663651624208769553, sar(96, mul(x, q))) q := add(204048457590392012362485061816622, sar(96, mul(x, q))) q := add(31853899698501571402653359427138, sar(96, mul(x, q))) q := add(909429971244387300277376558375, sar(96, mul(x, q))) // `p / q` is in the range `(0, 0.125) * 2**96`. // Finalization, we need to: // - Multiply by the scale factor `s = 5.549…`. // - Add `ln(2**96 / 10**18)`. // - Add `k * ln(2)`. // - Multiply by `10**18 / 2**96 = 5**18 >> 78`. // The q polynomial is known not to have zeros in the domain. // No scaling required because p is already `2**96` too large. p := sdiv(p, q) // Multiply by the scaling factor: `s * 5**18 * 2**96`, base is now `5**18 * 2**192`. p := mul(1677202110996718588342820967067443963516166, p) // Add `ln(2) * k * 5**18 * 2**192`. // forgefmt: disable-next-item p := add(mul(16597577552685614221487285958193947469193820559219878177908093499208371, sub(159, r)), p) // Add `ln(2**96 / 10**18) * 5**18 * 2**192`. p := add(600920179829731861736702779321621459595472258049074101567377883020018308, p) // Base conversion: mul `2**18 / 2**192`. r := sar(174, p) } } /// @dev Returns `W_0(x)`, denominated in `WAD`. /// See: https://en.wikipedia.org/wiki/Lambert_W_function /// a.k.a. Product log function. This is an approximation of the principal branch. function lambertW0Wad(int256 x) internal pure returns (int256 w) { // forgefmt: disable-next-item unchecked { if ((w = x) <= -367879441171442322) revert OutOfDomain(); // `x` less than `-1/e`. int256 wad = int256(WAD); int256 p = x; uint256 c; // Whether we need to avoid catastrophic cancellation. uint256 i = 4; // Number of iterations. if (w <= 0x1ffffffffffff) { if (-0x4000000000000 <= w) { i = 1; // Inputs near zero only take one step to converge. } else if (w <= -0x3ffffffffffffff) { i = 32; // Inputs near `-1/e` take very long to converge. } } else if (w >> 63 == 0) { /// @solidity memory-safe-assembly assembly { // Inline log2 for more performance, since the range is small. let v := shr(49, w) let l := shl(3, lt(0xff, v)) l := add(or(l, byte(and(0x1f, shr(shr(l, v), 0x8421084210842108cc6318c6db6d54be)), 0x0706060506020504060203020504030106050205030304010505030400000000)), 49) w := sdiv(shl(l, 7), byte(sub(l, 31), 0x0303030303030303040506080c13)) c := gt(l, 60) i := add(2, add(gt(l, 53), c)) } } else { int256 ll = lnWad(w = lnWad(w)); /// @solidity memory-safe-assembly assembly { // `w = ln(x) - ln(ln(x)) + b * ln(ln(x)) / ln(x)`. w := add(sdiv(mul(ll, 1023715080943847266), w), sub(w, ll)) i := add(3, iszero(shr(68, x))) c := iszero(shr(143, x)) } if (c == 0) { do { // If `x` is big, use Newton's so that intermediate values won't overflow. int256 e = expWad(w); /// @solidity memory-safe-assembly assembly { let t := mul(w, div(e, wad)) w := sub(w, sdiv(sub(t, x), div(add(e, t), wad))) } if (p <= w) break; p = w; } while (--i != 0); /// @solidity memory-safe-assembly assembly { w := sub(w, sgt(w, 2)) } return w; } } do { // Otherwise, use Halley's for faster convergence. int256 e = expWad(w); /// @solidity memory-safe-assembly assembly { let t := add(w, wad) let s := sub(mul(w, e), mul(x, wad)) w := sub(w, sdiv(mul(s, wad), sub(mul(e, t), sdiv(mul(add(t, wad), s), add(t, t))))) } if (p <= w) break; p = w; } while (--i != c); /// @solidity memory-safe-assembly assembly { w := sub(w, sgt(w, 2)) } // For certain ranges of `x`, we'll use the quadratic-rate recursive formula of // R. Iacono and J.P. Boyd for the last iteration, to avoid catastrophic cancellation. if (c != 0) { int256 t = w | 1; /// @solidity memory-safe-assembly assembly { x := sdiv(mul(x, wad), t) } x = (t * (wad + lnWad(x))); /// @solidity memory-safe-assembly assembly { w := sdiv(x, add(wad, t)) } } } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* GENERAL NUMBER UTILITIES */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Calculates `floor(x * y / d)` with full precision. /// Throws if result overflows a uint256 or when `d` is zero. /// Credit to Remco Bloemen under MIT license: https://2π.com/21/muldiv function fullMulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) { /// @solidity memory-safe-assembly assembly { for {} 1 {} { // 512-bit multiply `[p1 p0] = x * y`. // Compute the product mod `2**256` and mod `2**256 - 1` // then use the Chinese Remainder Theorem to reconstruct // the 512 bit result. The result is stored in two 256 // variables such that `product = p1 * 2**256 + p0`. // Least significant 256 bits of the product. result := mul(x, y) // Temporarily use `result` as `p0` to save gas. let mm := mulmod(x, y, not(0)) // Most significant 256 bits of the product. let p1 := sub(mm, add(result, lt(mm, result))) // Handle non-overflow cases, 256 by 256 division. if iszero(p1) { if iszero(d) { mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. revert(0x1c, 0x04) } result := div(result, d) break } // Make sure the result is less than `2**256`. Also prevents `d == 0`. if iszero(gt(d, p1)) { mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. revert(0x1c, 0x04) } /*------------------- 512 by 256 division --------------------*/ // Make division exact by subtracting the remainder from `[p1 p0]`. // Compute remainder using mulmod. let r := mulmod(x, y, d) // `t` is the least significant bit of `d`. // Always greater or equal to 1. let t := and(d, sub(0, d)) // Divide `d` by `t`, which is a power of two. d := div(d, t) // Invert `d mod 2**256` // Now that `d` is an odd number, it has an inverse // modulo `2**256` such that `d * inv = 1 mod 2**256`. // Compute the inverse by starting with a seed that is correct // correct for four bits. That is, `d * inv = 1 mod 2**4`. let inv := xor(2, mul(3, d)) // Now use Newton-Raphson iteration to improve the precision. // Thanks to Hensel's lifting lemma, this also works in modular // arithmetic, doubling the correct bits in each step. inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**8 inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**16 inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**32 inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**64 inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**128 result := mul( // Divide [p1 p0] by the factors of two. // Shift in bits from `p1` into `p0`. For this we need // to flip `t` such that it is `2**256 / t`. or( mul(sub(p1, gt(r, result)), add(div(sub(0, t), t), 1)), div(sub(result, r), t) ), // inverse mod 2**256 mul(inv, sub(2, mul(d, inv))) ) break } } } /// @dev Calculates `floor(x * y / d)` with full precision, rounded up. /// Throws if result overflows a uint256 or when `d` is zero. /// Credit to Uniswap-v3-core under MIT license: /// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol function fullMulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) { result = fullMulDiv(x, y, d); /// @solidity memory-safe-assembly assembly { if mulmod(x, y, d) { result := add(result, 1) if iszero(result) { mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. revert(0x1c, 0x04) } } } } /// @dev Returns `floor(x * y / d)`. /// Reverts if `x * y` overflows, or `d` is zero. function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y)) if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) { mstore(0x00, 0xad251c27) // `MulDivFailed()`. revert(0x1c, 0x04) } z := div(mul(x, y), d) } } /// @dev Returns `ceil(x * y / d)`. /// Reverts if `x * y` overflows, or `d` is zero. function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y)) if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) { mstore(0x00, 0xad251c27) // `MulDivFailed()`. revert(0x1c, 0x04) } z := add(iszero(iszero(mod(mul(x, y), d))), div(mul(x, y), d)) } } /// @dev Returns `ceil(x / d)`. /// Reverts if `d` is zero. function divUp(uint256 x, uint256 d) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { if iszero(d) { mstore(0x00, 0x65244e4e) // `DivFailed()`. revert(0x1c, 0x04) } z := add(iszero(iszero(mod(x, d))), div(x, d)) } } /// @dev Returns `max(0, x - y)`. function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := mul(gt(x, y), sub(x, y)) } } /// @dev Exponentiate `x` to `y` by squaring, denominated in base `b`. /// Reverts if the computation overflows. function rpow(uint256 x, uint256 y, uint256 b) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := mul(b, iszero(y)) // `0 ** 0 = 1`. Otherwise, `0 ** n = 0`. if x { z := xor(b, mul(xor(b, x), and(y, 1))) // `z = isEven(y) ? scale : x` let half := shr(1, b) // Divide `b` by 2. // Divide `y` by 2 every iteration. for { y := shr(1, y) } y { y := shr(1, y) } { let xx := mul(x, x) // Store x squared. let xxRound := add(xx, half) // Round to the nearest number. // Revert if `xx + half` overflowed, or if `x ** 2` overflows. if or(lt(xxRound, xx), shr(128, x)) { mstore(0x00, 0x49f7642b) // `RPowOverflow()`. revert(0x1c, 0x04) } x := div(xxRound, b) // Set `x` to scaled `xxRound`. // If `y` is odd: if and(y, 1) { let zx := mul(z, x) // Compute `z * x`. let zxRound := add(zx, half) // Round to the nearest number. // If `z * x` overflowed or `zx + half` overflowed: if or(xor(div(zx, x), z), lt(zxRound, zx)) { // Revert if `x` is non-zero. if iszero(iszero(x)) { mstore(0x00, 0x49f7642b) // `RPowOverflow()`. revert(0x1c, 0x04) } } z := div(zxRound, b) // Return properly scaled `zxRound`. } } } } } /// @dev Returns the square root of `x`. function sqrt(uint256 x) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { // `floor(sqrt(2**15)) = 181`. `sqrt(2**15) - 181 = 2.84`. z := 181 // The "correct" value is 1, but this saves a multiplication later. // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. // Let `y = x / 2**r`. We check `y >= 2**(k + 8)` // but shift right by `k` bits to ensure that if `x >= 256`, then `y >= 256`. let r := shl(7, lt(0xffffffffffffffffffffffffffffffffff, x)) r := or(r, shl(6, lt(0xffffffffffffffffff, shr(r, x)))) r := or(r, shl(5, lt(0xffffffffff, shr(r, x)))) r := or(r, shl(4, lt(0xffffff, shr(r, x)))) z := shl(shr(1, r), z) // Goal was to get `z*z*y` within a small factor of `x`. More iterations could // get y in a tighter range. Currently, we will have y in `[256, 256*(2**16))`. // We ensured `y >= 256` so that the relative difference between `y` and `y+1` is small. // That's not possible if `x < 256` but we can just verify those cases exhaustively. // Now, `z*z*y <= x < z*z*(y+1)`, and `y <= 2**(16+8)`, and either `y >= 256`, or `x < 256`. // Correctness can be checked exhaustively for `x < 256`, so we assume `y >= 256`. // Then `z*sqrt(y)` is within `sqrt(257)/sqrt(256)` of `sqrt(x)`, or about 20bps. // For `s` in the range `[1/256, 256]`, the estimate `f(s) = (181/1024) * (s+1)` // is in the range `(1/2.84 * sqrt(s), 2.84 * sqrt(s))`, // with largest error when `s = 1` and when `s = 256` or `1/256`. // Since `y` is in `[256, 256*(2**16))`, let `a = y/65536`, so that `a` is in `[1/256, 256)`. // Then we can estimate `sqrt(y)` using // `sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2**18`. // There is no overflow risk here since `y < 2**136` after the first branch above. z := shr(18, mul(z, add(shr(r, x), 65536))) // A `mul()` is saved from starting `z` at 181. // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) z := shr(1, add(z, div(x, z))) // If `x+1` is a perfect square, the Babylonian method cycles between // `floor(sqrt(x))` and `ceil(sqrt(x))`. This statement ensures we return floor. // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division z := sub(z, lt(div(x, z), z)) } } /// @dev Returns the cube root of `x`. /// Credit to bout3fiddy and pcaversaccio under AGPLv3 license: /// https://github.com/pcaversaccio/snekmate/blob/main/src/utils/Math.vy function cbrt(uint256 x) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { let r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) r := or(r, shl(4, lt(0xffff, shr(r, x)))) r := or(r, shl(3, lt(0xff, shr(r, x)))) z := div(shl(div(r, 3), shl(lt(0xf, shr(r, x)), 0xf)), xor(7, mod(r, 3))) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := div(add(add(div(x, mul(z, z)), z), z), 3) z := sub(z, lt(div(x, mul(z, z)), z)) } } /// @dev Returns the square root of `x`, denominated in `WAD`. function sqrtWad(uint256 x) internal pure returns (uint256 z) { unchecked { z = 10 ** 9; if (x <= type(uint256).max / 10 ** 36 - 1) { x *= 10 ** 18; z = 1; } z *= sqrt(x); } } /// @dev Returns the cube root of `x`, denominated in `WAD`. function cbrtWad(uint256 x) internal pure returns (uint256 z) { unchecked { z = 10 ** 12; if (x <= (type(uint256).max / 10 ** 36) * 10 ** 18 - 1) { if (x >= type(uint256).max / 10 ** 36) { x *= 10 ** 18; z = 10 ** 6; } else { x *= 10 ** 36; z = 1; } } z *= cbrt(x); } } /// @dev Returns the factorial of `x`. function factorial(uint256 x) internal pure returns (uint256 result) { /// @solidity memory-safe-assembly assembly { if iszero(lt(x, 58)) { mstore(0x00, 0xaba0f2a2) // `FactorialOverflow()`. revert(0x1c, 0x04) } for { result := 1 } x { x := sub(x, 1) } { result := mul(result, x) } } } /// @dev Returns the log2 of `x`. /// Equivalent to computing the index of the most significant bit (MSB) of `x`. /// Returns 0 if `x` is zero. function log2(uint256 x) internal pure returns (uint256 r) { /// @solidity memory-safe-assembly assembly { r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) r := or(r, shl(4, lt(0xffff, shr(r, x)))) r := or(r, shl(3, lt(0xff, shr(r, x)))) // forgefmt: disable-next-item r := or(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), 0x0706060506020504060203020504030106050205030304010505030400000000)) } } /// @dev Returns the log2 of `x`, rounded up. /// Returns 0 if `x` is zero. function log2Up(uint256 x) internal pure returns (uint256 r) { r = log2(x); /// @solidity memory-safe-assembly assembly { r := add(r, lt(shl(r, 1), x)) } } /// @dev Returns the log10 of `x`. /// Returns 0 if `x` is zero. function log10(uint256 x) internal pure returns (uint256 r) { /// @solidity memory-safe-assembly assembly { if iszero(lt(x, 100000000000000000000000000000000000000)) { x := div(x, 100000000000000000000000000000000000000) r := 38 } if iszero(lt(x, 100000000000000000000)) { x := div(x, 100000000000000000000) r := add(r, 20) } if iszero(lt(x, 10000000000)) { x := div(x, 10000000000) r := add(r, 10) } if iszero(lt(x, 100000)) { x := div(x, 100000) r := add(r, 5) } r := add(r, add(gt(x, 9), add(gt(x, 99), add(gt(x, 999), gt(x, 9999))))) } } /// @dev Returns the log10 of `x`, rounded up. /// Returns 0 if `x` is zero. function log10Up(uint256 x) internal pure returns (uint256 r) { r = log10(x); /// @solidity memory-safe-assembly assembly { r := add(r, lt(exp(10, r), x)) } } /// @dev Returns the log256 of `x`. /// Returns 0 if `x` is zero. function log256(uint256 x) internal pure returns (uint256 r) { /// @solidity memory-safe-assembly assembly { r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) r := or(r, shl(4, lt(0xffff, shr(r, x)))) r := or(shr(3, r), lt(0xff, shr(r, x))) } } /// @dev Returns the log256 of `x`, rounded up. /// Returns 0 if `x` is zero. function log256Up(uint256 x) internal pure returns (uint256 r) { r = log256(x); /// @solidity memory-safe-assembly assembly { r := add(r, lt(shl(shl(3, r), 1), x)) } } /// @dev Returns the scientific notation format `mantissa * 10 ** exponent` of `x`. /// Useful for compressing prices (e.g. using 25 bit mantissa and 7 bit exponent). function sci(uint256 x) internal pure returns (uint256 mantissa, uint256 exponent) { /// @solidity memory-safe-assembly assembly { mantissa := x if mantissa { if iszero(mod(mantissa, 1000000000000000000000000000000000)) { mantissa := div(mantissa, 1000000000000000000000000000000000) exponent := 33 } if iszero(mod(mantissa, 10000000000000000000)) { mantissa := div(mantissa, 10000000000000000000) exponent := add(exponent, 19) } if iszero(mod(mantissa, 1000000000000)) { mantissa := div(mantissa, 1000000000000) exponent := add(exponent, 12) } if iszero(mod(mantissa, 1000000)) { mantissa := div(mantissa, 1000000) exponent := add(exponent, 6) } if iszero(mod(mantissa, 10000)) { mantissa := div(mantissa, 10000) exponent := add(exponent, 4) } if iszero(mod(mantissa, 100)) { mantissa := div(mantissa, 100) exponent := add(exponent, 2) } if iszero(mod(mantissa, 10)) { mantissa := div(mantissa, 10) exponent := add(exponent, 1) } } } } /// @dev Convenience function for packing `x` into a smaller number using `sci`. /// The `mantissa` will be in bits [7..255] (the upper 249 bits). /// The `exponent` will be in bits [0..6] (the lower 7 bits). /// Use `SafeCastLib` to safely ensure that the `packed` number is small /// enough to fit in the desired unsigned integer type: /// ``` /// uint32 packed = SafeCastLib.toUint32(FixedPointMathLib.packSci(777 ether)); /// ``` function packSci(uint256 x) internal pure returns (uint256 packed) { (x, packed) = sci(x); // Reuse for `mantissa` and `exponent`. /// @solidity memory-safe-assembly assembly { if shr(249, x) { mstore(0x00, 0xce30380c) // `MantissaOverflow()`. revert(0x1c, 0x04) } packed := or(shl(7, x), packed) } } /// @dev Convenience function for unpacking a packed number from `packSci`. function unpackSci(uint256 packed) internal pure returns (uint256 unpacked) { unchecked { unpacked = (packed >> 7) * 10 ** (packed & 0x7f); } } /// @dev Returns the average of `x` and `y`. function avg(uint256 x, uint256 y) internal pure returns (uint256 z) { unchecked { z = (x & y) + ((x ^ y) >> 1); } } /// @dev Returns the average of `x` and `y`. function avg(int256 x, int256 y) internal pure returns (int256 z) { unchecked { z = (x >> 1) + (y >> 1) + (x & y & 1); } } /// @dev Returns the absolute value of `x`. function abs(int256 x) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(sar(255, x), add(sar(255, x), x)) } } /// @dev Returns the absolute distance between `x` and `y`. function dist(int256 x, int256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(mul(xor(sub(y, x), sub(x, y)), sgt(x, y)), sub(y, x)) } } /// @dev Returns the minimum of `x` and `y`. function min(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, y), lt(y, x))) } } /// @dev Returns the minimum of `x` and `y`. function min(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, y), slt(y, x))) } } /// @dev Returns the maximum of `x` and `y`. function max(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, y), gt(y, x))) } } /// @dev Returns the maximum of `x` and `y`. function max(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, y), sgt(y, x))) } } /// @dev Returns `x`, bounded to `minValue` and `maxValue`. function clamp(uint256 x, uint256 minValue, uint256 maxValue) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, minValue), gt(minValue, x))) z := xor(z, mul(xor(z, maxValue), lt(maxValue, z))) } } /// @dev Returns `x`, bounded to `minValue` and `maxValue`. function clamp(int256 x, int256 minValue, int256 maxValue) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := xor(x, mul(xor(x, minValue), sgt(minValue, x))) z := xor(z, mul(xor(z, maxValue), slt(maxValue, z))) } } /// @dev Returns greatest common divisor of `x` and `y`. function gcd(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { for { z := x } y {} { let t := y y := mod(z, y) z := t } } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* RAW NUMBER OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Returns `x + y`, without checking for overflow. function rawAdd(uint256 x, uint256 y) internal pure returns (uint256 z) { unchecked { z = x + y; } } /// @dev Returns `x + y`, without checking for overflow. function rawAdd(int256 x, int256 y) internal pure returns (int256 z) { unchecked { z = x + y; } } /// @dev Returns `x - y`, without checking for underflow. function rawSub(uint256 x, uint256 y) internal pure returns (uint256 z) { unchecked { z = x - y; } } /// @dev Returns `x - y`, without checking for underflow. function rawSub(int256 x, int256 y) internal pure returns (int256 z) { unchecked { z = x - y; } } /// @dev Returns `x * y`, without checking for overflow. function rawMul(uint256 x, uint256 y) internal pure returns (uint256 z) { unchecked { z = x * y; } } /// @dev Returns `x * y`, without checking for overflow. function rawMul(int256 x, int256 y) internal pure returns (int256 z) { unchecked { z = x * y; } } /// @dev Returns `x / y`, returning 0 if `y` is zero. function rawDiv(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := div(x, y) } } /// @dev Returns `x / y`, returning 0 if `y` is zero. function rawSDiv(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := sdiv(x, y) } } /// @dev Returns `x % y`, returning 0 if `y` is zero. function rawMod(uint256 x, uint256 y) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := mod(x, y) } } /// @dev Returns `x % y`, returning 0 if `y` is zero. function rawSMod(int256 x, int256 y) internal pure returns (int256 z) { /// @solidity memory-safe-assembly assembly { z := smod(x, y) } } /// @dev Returns `(x + y) % d`, return 0 if `d` if zero. function rawAddMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := addmod(x, y, d) } } /// @dev Returns `(x * y) % d`, return 0 if `d` if zero. function rawMulMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { /// @solidity memory-safe-assembly assembly { z := mulmod(x, y, d) } } }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; /// @notice Library for generating pseudorandom numbers. /// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibPRNG.sol) /// @author LazyShuffler based on NextShuffler by aschlosberg (divergencearran) /// (https://github.com/divergencetech/ethier/blob/main/contracts/random/NextShuffler.sol) library LibPRNG { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CUSTOM ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev The initial length must be greater than zero and less than `2**32 - 1`. error InvalidInitialLazyShufflerLength(); /// @dev The new length must not be less than the current length. error InvalidNewLazyShufflerLength(); /// @dev The lazy shuffler has not been initialized. error LazyShufflerNotInitialized(); /// @dev Cannot double initialize the lazy shuffler. error LazyShufflerAlreadyInitialized(); /// @dev The lazy shuffle has finished. error LazyShuffleFinished(); /// @dev The queried index is out of bounds. error LazyShufflerGetOutOfBounds(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev The scalar of ETH and most ERC20s. uint256 internal constant WAD = 1e18; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STRUCTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev A pseudorandom number state in memory. struct PRNG { uint256 state; } /// @dev A lazy Fisher-Yates shuffler for a range `[0..n)` in storage. struct LazyShuffler { // Bits Layout: // - [0..31] `numShuffled` // - [32..223] `permutationSlot` // - [224..255] `length` uint256 _state; } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Seeds the `prng` with `state`. function seed(PRNG memory prng, uint256 state) internal pure { /// @solidity memory-safe-assembly assembly { mstore(prng, state) } } /// @dev Returns the next pseudorandom uint256. /// All bits of the returned uint256 pass the NIST Statistical Test Suite. function next(PRNG memory prng) internal pure returns (uint256 result) { // We simply use `keccak256` for a great balance between // runtime gas costs, bytecode size, and statistical properties. // // A high-quality LCG with a 32-byte state // is only about 30% more gas efficient during runtime, // but requires a 32-byte multiplier, which can cause bytecode bloat // when this function is inlined. // // Using this method is about 2x more efficient than // `nextRandomness = uint256(keccak256(abi.encode(randomness)))`. /// @solidity memory-safe-assembly assembly { result := keccak256(prng, 0x20) mstore(prng, result) } } /// @dev Returns a pseudorandom uint256, uniformly distributed /// between 0 (inclusive) and `upper` (exclusive). /// If your modulus is big, this method is recommended /// for uniform sampling to avoid modulo bias. /// For uniform sampling across all uint256 values, /// or for small enough moduli such that the bias is neligible, /// use {next} instead. function uniform(PRNG memory prng, uint256 upper) internal pure returns (uint256 result) { /// @solidity memory-safe-assembly assembly { for {} 1 {} { result := keccak256(prng, 0x20) mstore(prng, result) if iszero(lt(result, mod(sub(0, upper), upper))) { break } } result := mod(result, upper) } } /// @dev Shuffles the array in-place with Fisher-Yates shuffle. function shuffle(PRNG memory prng, uint256[] memory a) internal pure { /// @solidity memory-safe-assembly assembly { let n := mload(a) let w := not(0) let mask := shr(128, w) if n { for { a := add(a, 0x20) } 1 {} { // We can just directly use `keccak256`, cuz // the other approaches don't save much. let r := keccak256(prng, 0x20) mstore(prng, r) // Note that there will be a very tiny modulo bias // if the length of the array is not a power of 2. // For all practical purposes, it is negligible // and will not be a fairness or security concern. { let j := add(a, shl(5, mod(shr(128, r), n))) n := add(n, w) // `sub(n, 1)`. if iszero(n) { break } let i := add(a, shl(5, n)) let t := mload(i) mstore(i, mload(j)) mstore(j, t) } { let j := add(a, shl(5, mod(and(r, mask), n))) n := add(n, w) // `sub(n, 1)`. if iszero(n) { break } let i := add(a, shl(5, n)) let t := mload(i) mstore(i, mload(j)) mstore(j, t) } } } } } /// @dev Shuffles the bytes in-place with Fisher-Yates shuffle. function shuffle(PRNG memory prng, bytes memory a) internal pure { /// @solidity memory-safe-assembly assembly { let n := mload(a) let w := not(0) let mask := shr(128, w) if n { let b := add(a, 0x01) for { a := add(a, 0x20) } 1 {} { // We can just directly use `keccak256`, cuz // the other approaches don't save much. let r := keccak256(prng, 0x20) mstore(prng, r) // Note that there will be a very tiny modulo bias // if the length of the array is not a power of 2. // For all practical purposes, it is negligible // and will not be a fairness or security concern. { let o := mod(shr(128, r), n) n := add(n, w) // `sub(n, 1)`. if iszero(n) { break } let t := mload(add(b, n)) mstore8(add(a, n), mload(add(b, o))) mstore8(add(a, o), t) } { let o := mod(and(r, mask), n) n := add(n, w) // `sub(n, 1)`. if iszero(n) { break } let t := mload(add(b, n)) mstore8(add(a, n), mload(add(b, o))) mstore8(add(a, o), t) } } } } } /// @dev Returns a sample from the standard normal distribution denominated in `WAD`. function standardNormalWad(PRNG memory prng) internal pure returns (int256 result) { /// @solidity memory-safe-assembly assembly { // Technically, this is the Irwin-Hall distribution with 20 samples. // The chance of drawing a sample outside 10 σ from the standard normal distribution // is ≈ 0.000000000000000000000015, which is insignificant for most practical purposes. // Passes the Kolmogorov-Smirnov test for 200k samples. Uses about 322 gas. result := keccak256(prng, 0x20) mstore(prng, result) let n := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43 // Prime. let a := 0x100000000000000000000000000000051 // Prime and a primitive root of `n`. let m := 0x1fffffffffffffff1fffffffffffffff1fffffffffffffff1fffffffffffffff let s := 0x1000000000000000100000000000000010000000000000001 let r1 := mulmod(result, a, n) let r2 := mulmod(r1, a, n) let r3 := mulmod(r2, a, n) // forgefmt: disable-next-item result := sub(sar(96, mul(26614938895861601847173011183, add(add(shr(192, mul(s, add(and(m, result), and(m, r1)))), shr(192, mul(s, add(and(m, r2), and(m, r3))))), shr(192, mul(s, and(m, mulmod(r3, a, n))))))), 7745966692414833770) } } /// @dev Returns a sample from the unit exponential distribution denominated in `WAD`. function exponentialWad(PRNG memory prng) internal pure returns (uint256 result) { /// @solidity memory-safe-assembly assembly { // Passes the Kolmogorov-Smirnov test for 200k samples. // Gas usage varies, starting from about 172+ gas. let r := keccak256(prng, 0x20) mstore(prng, r) let p := shl(129, r) let w := shl(1, r) if iszero(gt(w, p)) { let n := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43 // Prime. let a := 0x100000000000000000000000000000051 // Prime and a primitive root of `n`. for {} 1 {} { r := mulmod(r, a, n) if iszero(lt(shl(129, r), w)) { r := mulmod(r, a, n) result := add(1000000000000000000, result) w := shl(1, r) p := shl(129, r) if iszero(lt(w, p)) { break } continue } w := shl(1, r) if iszero(lt(w, shl(129, r))) { break } } } result := add(div(p, shl(129, 170141183460469231732)), result) } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE-BASED RANGE LAZY SHUFFLING OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Initializes the state for lazy-shuffling the range `[0..n)`. /// Reverts if `n == 0 || n >= 2**32 - 1`. /// Reverts if `$` has already been initialized. /// If you need to reduce the length after initialization, just use a fresh new `$`. function initialize(LazyShuffler storage $, uint256 n) internal { /// @solidity memory-safe-assembly assembly { if iszero(lt(sub(n, 1), 0xfffffffe)) { mstore(0x00, 0x83b53941) // `InvalidInitialLazyShufflerLength()`. revert(0x1c, 0x04) } if sload($.slot) { mstore(0x00, 0x0c9f11f2) // `LazyShufflerAlreadyInitialized()`. revert(0x1c, 0x04) } mstore(0x00, $.slot) sstore($.slot, or(shl(224, n), shl(32, shr(64, keccak256(0x00, 0x20))))) } } /// @dev Increases the length of `$`. /// Reverts if `$` has not been initialized. function grow(LazyShuffler storage $, uint256 n) internal { /// @solidity memory-safe-assembly assembly { let state := sload($.slot) // The packed value at `$`. // If the new length is smaller than the old length, revert. if lt(n, shr(224, state)) { mstore(0x00, 0xbed37c6e) // `InvalidNewLazyShufflerLength()`. revert(0x1c, 0x04) } if iszero(state) { mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`. revert(0x1c, 0x04) } sstore($.slot, or(shl(224, n), shr(32, shl(32, state)))) } } /// @dev Restarts the shuffler by setting `numShuffled` to zero, /// such that all elements can be drawn again. /// Restarting does NOT clear the internal permutation, nor changes the length. /// Even with the same sequence of randomness, reshuffling can yield different results. function restart(LazyShuffler storage $) internal { /// @solidity memory-safe-assembly assembly { let state := sload($.slot) if iszero(state) { mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`. revert(0x1c, 0x04) } sstore($.slot, shl(32, shr(32, state))) } } /// @dev Returns the number of elements that have been shuffled. function numShuffled(LazyShuffler storage $) internal view returns (uint256 result) { /// @solidity memory-safe-assembly assembly { result := and(0xffffffff, sload($.slot)) } } /// @dev Returns the length of `$`. /// Returns zero if `$` is not initialized, else a non-zero value less than `2**32 - 1`. function length(LazyShuffler storage $) internal view returns (uint256 result) { /// @solidity memory-safe-assembly assembly { result := shr(224, sload($.slot)) } } /// @dev Returns if `$` has been initialized. function initialized(LazyShuffler storage $) internal view returns (bool result) { /// @solidity memory-safe-assembly assembly { result := iszero(iszero(sload($.slot))) } } /// @dev Returns if there are any more elements left to shuffle. /// Reverts if `$` is not initialized. function finished(LazyShuffler storage $) internal view returns (bool result) { /// @solidity memory-safe-assembly assembly { let state := sload($.slot) // The packed value at `$`. if iszero(state) { mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`. revert(0x1c, 0x04) } result := eq(shr(224, state), and(0xffffffff, state)) } } /// @dev Returns the current value stored at `index`, accounting for all historical shuffling. /// Reverts if `index` is greater than or equal to the `length` of `$`. function get(LazyShuffler storage $, uint256 index) internal view returns (uint256 result) { /// @solidity memory-safe-assembly assembly { let state := sload($.slot) // The packed value at `$`. let n := shr(224, state) // Length of `$`. if iszero(lt(index, n)) { mstore(0x00, 0x61367cc4) // `LazyShufflerGetOutOfBounds()`. revert(0x1c, 0x04) } let u32 := gt(n, 0xfffe) let s := add(shr(sub(4, u32), index), shr(64, shl(32, state))) // Bucket slot. let o := shl(add(4, u32), and(index, shr(u32, 15))) // Bucket slot offset (bits). let m := sub(shl(shl(u32, 16), 1), 1) // Value mask. result := and(m, shr(o, sload(s))) result := xor(index, mul(xor(index, sub(result, 1)), iszero(iszero(result)))) } } /// @dev Does a single Fisher-Yates shuffle step, increments the `numShuffled` in `$`, /// and returns the next value in the shuffled range. /// `randomness` can be taken from a good-enough source, or a higher quality source like VRF. /// Reverts if there are no more values to shuffle, which includes the case if `$` is not initialized. function next(LazyShuffler storage $, uint256 randomness) internal returns (uint256 chosen) { /// @solidity memory-safe-assembly assembly { function _get(u32_, state_, i_) -> _value { let s_ := add(shr(sub(4, u32_), i_), shr(64, shl(32, state_))) // Bucket slot. let o_ := shl(add(4, u32_), and(i_, shr(u32_, 15))) // Bucket slot offset (bits). let m_ := sub(shl(shl(u32_, 16), 1), 1) // Value mask. _value := and(m_, shr(o_, sload(s_))) _value := xor(i_, mul(xor(i_, sub(_value, 1)), iszero(iszero(_value)))) } function _set(u32_, state_, i_, value_) { let s_ := add(shr(sub(4, u32_), i_), shr(64, shl(32, state_))) // Bucket slot. let o_ := shl(add(4, u32_), and(i_, shr(u32_, 15))) // Bucket slot offset (bits). let m_ := sub(shl(shl(u32_, 16), 1), 1) // Value mask. let v_ := sload(s_) // Bucket slot value. value_ := mul(iszero(eq(i_, value_)), add(value_, 1)) sstore(s_, xor(v_, shl(o_, and(m_, xor(shr(o_, v_), value_))))) } let state := sload($.slot) // The packed value at `$`. let shuffled := and(0xffffffff, state) // Number of elements shuffled. let n := shr(224, state) // Length of `$`. let remainder := sub(n, shuffled) // Number of elements left to shuffle. if iszero(remainder) { mstore(0x00, 0x51065f79) // `LazyShuffleFinished()`. revert(0x1c, 0x04) } mstore(0x00, randomness) // (Re)hash the randomness so that we don't mstore(0x20, shuffled) // need to expect guarantees on its distribution. let index := add(mod(keccak256(0x00, 0x40), remainder), shuffled) chosen := _get(gt(n, 0xfffe), state, index) _set(gt(n, 0xfffe), state, index, _get(gt(n, 0xfffe), state, shuffled)) _set(gt(n, 0xfffe), state, shuffled, chosen) sstore($.slot, add(1, state)) // Increment the `numShuffled` by 1, and store it. } } }
// SPDX-License-Identifier: BUSL-1.1 /** ▒▓▒ ▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓▓▒ ▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓██▓ ▒██▒▓▓▓▓█▓██████████████████▓ ▒▒▒▓███████████████▒ ▒██▒▓█████████████████████▒ ▒▓██████████▓███████ ▒███████████▓▒ ▒███▓▓██████▓ █████████▒ ▒▓▒▓███████▒ ███████▓ ▒▒▒▒▒▓▓█▓▒ ▓█▓████████ ▒▒▒▒▒ ▒▒▒▒▓▓▓█████▒ ▓█████████▓ ▒▓▓▓▒▓██████▓ ▒▓▓████████▒ ▒██▓▓▓███████▒ ▒▒▓███▓████ ▒███▓█████▒ ▒▒█████▓██▓ ██████▓ ▒▒▒▓██▓██▓█████▒ ▒▒▓▓▒ ▒██▓▒▓▓████████ ▓█████▓███████▓ ██▓▓██████████▒ ▒█████████████ ███████████▓ ▒▓▓▓▓▓▓▒▓ ▒█████████▒ ▒▓▓ ▒▓█▒ ▒▒█▒▒ ▓██████ ▒▒▓▓▒ ▒▒█▒ ▓▒ ▒████ ▒▓█▓█▓▒ ▓▒██▓▒ ██ ▒▓█▓▓▓██▒ ▓█▓▓▓▓▓█▓▓▓▒ ▒▒▒ ▒▒▒▓▓▓▓▒▓▒▒▓▒▓▓▓▓▓▓▓▓▒ ▒▓█▒ ▒▓▒▓█▓ ▒▓█▓▓▓▓▓▓▓▓▓▓▒ ▒▒▒▓▒ ▒▒▒▓▓ ▓▓ ▓▓█▓ ▒▒▓▓ ▒▒█▒ ▒▓▒▓█▓ ▒▒▓▓▓▒▓▒ ▒▓▓▓▒█▒ ▒▒▒█▒ ▒▒█▓▒▒▒▓▓▓▒ ▓██▓▓▓▓▓▓▓███▓ ▒ ▒▓▓█▓ ▒▓▓▓▓█▓█▓ ▒█▓▓▒ ▓▓█▓▒▓█▓▒▒ ▓█▓ ▓███▓ ▓▓▒ ▒▒▓▓█▓▒▒▓█▒ ▒▓██▓ ▓██▓▒ ▒█▓ ▓▓██ ▒▓▓▓▒▒▓█▓ ▒▓████▒ ██▓▓▒▒▒▒▓▓███▓▒ ▒▓▓▓▓▒▒ ▒▓▓▓▓▓▓▓▒▒▒▓█▓▓▓▓█▓▓▒▒▓▓▓▓▓▒ ▒▓████▓▒ ▓▓███████▓▓▒ */ pragma solidity 0.8.23; import "../interfaces/IAppErrors.sol"; import "../interfaces/IERC20.sol"; import "../interfaces/IERC721.sol"; import "../interfaces/IGuildBank.sol"; import "../interfaces/IGuildController.sol"; import "../openzeppelin/ERC721Holder.sol"; import "../interfaces/IApplicationEvents.sol"; /// @dev SCR-1253: ERC721Holder was added in 1.0.2. /// So, safeTransferFrom won't work with all contract instances with the lower version. They should be upgraded. contract GuildBank is IGuildBank, ERC721Holder { //region ------------------------ CONSTANTS /// @notice Version of the contract /// @dev Should be incremented when contract changed string public constant VERSION = "1.0.2"; //endregion ------------------------ CONSTANTS //region ------------------------ Members IGuildController immutable public guildController; uint immutable public guildId; //endregion ------------------------ Members //region ------------------------ Restrictions and constructor function _onlyGuildController(address msgSender) internal view { if (msgSender != address(guildController)) revert IAppErrors.GuildControllerOnly(); } constructor (address guildController_, uint guildId_) { guildController = IGuildController(guildController_); guildId = guildId_; } //endregion ------------------------ Restrictions and constructor //region ------------------------ ERC20 function transfer(address token, address recipient, uint amount) external { _onlyGuildController(msg.sender); IERC20(token).transfer(recipient, amount); emit IApplicationEvents.GuildBankTransfer(token, recipient, amount); } function approve(address token, address spender, uint256 amount) external returns (bool) { _onlyGuildController(msg.sender); return IERC20(token).approve(spender, amount); } //endregion ------------------------ ERC20 //region ------------------------ ERC721 function transferNft(address to, address nft, uint256 tokenId) external { _onlyGuildController(msg.sender); IERC721(nft).transferFrom(address(this), to, tokenId); emit IApplicationEvents.GuildBankTransferNft(to, nft, tokenId); } function transferNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external { _onlyGuildController(msg.sender); uint len = nfts.length; if (len != tokenIds.length) revert IAppErrors.LengthsMismatch(); for (uint i; i < len; ++i) { IERC721(nfts[i]).transferFrom(address(this), to, tokenIds[i]); } emit IApplicationEvents.GuildBankTransferNftMulti(to, nfts, tokenIds); } function approveNft(address to, address nft, uint256 tokenId) external { _onlyGuildController(msg.sender); IERC721(nft).approve(to, tokenId); } function approveNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external { _onlyGuildController(msg.sender); uint len = nfts.length; if (len != tokenIds.length) revert IAppErrors.LengthsMismatch(); for (uint i; i < len; ++i) { IERC721(nfts[i]).approve(to, tokenIds[i]); } } //endregion ------------------------ ERC721 }
{ "evmVersion": "istanbul", "optimizer": { "enabled": true, "runs": 50 }, "outputSelection": { "*": { "*": [ "evm.bytecode", "evm.deployedBytecode", "devdoc", "userdoc", "metadata", "abi" ] } }, "metadata": { "useLiteralContent": true }, "libraries": { "contracts/lib/PvpFightLib.sol": { "PvpFightLib": "0x0caef1783c939632f8c890efb8a8d43935366584" } } }
Contract Security Audit
- No Contract Security Audit Submitted- Submit Audit Here
[{"inputs":[],"name":"BiomeAlreadySelected","type":"error"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"}],"name":"EnumerableMapNonexistentKey","type":"error"},{"inputs":[{"internalType":"address","name":"heroToken","type":"address"},{"internalType":"uint256","name":"heroTokenId","type":"uint256"}],"name":"ErrorHeroIsDead","type":"error"},{"inputs":[{"internalType":"address","name":"heroToken","type":"address"}],"name":"ErrorHeroIsNotRegistered","type":"error"},{"inputs":[{"internalType":"uint256","name":"biome","type":"uint256"}],"name":"ErrorIncorrectBiome","type":"error"},{"inputs":[{"internalType":"uint256","name":"heroLevel","type":"uint256"}],"name":"ErrorLevelTooLow","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"ErrorNotDeployer","type":"error"},{"inputs":[],"name":"ErrorNotGuildController","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ErrorNotOwner","type":"error"},{"inputs":[],"name":"ErrorPaused","type":"error"},{"inputs":[{"internalType":"uint256","name":"right","type":"uint256"}],"name":"GuildActionForbidden","type":"error"},{"inputs":[],"name":"HeroInDungeon","type":"error"},{"inputs":[],"name":"HeroWasTransferredBetweenAccounts","type":"error"},{"inputs":[],"name":"InvalidInitialization","type":"error"},{"inputs":[],"name":"NoDominationRequest","type":"error"},{"inputs":[],"name":"NotGuildMember","type":"error"},{"inputs":[],"name":"NotInitializing","type":"error"},{"inputs":[],"name":"PvpFightOpponentNotFound","type":"error"},{"inputs":[],"name":"PvpHeroHasInitializedFight","type":"error"},{"inputs":[],"name":"PvpHeroNotRegistered","type":"error"},{"inputs":[],"name":"SandboxModeNotAllowed","type":"error"},{"inputs":[{"internalType":"address","name":"heroToken","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"}],"name":"Staked","type":"error"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"TooHighValue","type":"error"},{"inputs":[],"name":"TooLowGuildLevel","type":"error"},{"inputs":[],"name":"UnknownPvpStrategy","type":"error"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"UserHasRegisteredPvpHeroInBiome","type":"error"},{"inputs":[{"internalType":"uint256","name":"week","type":"uint256"}],"name":"UserNotAllowedForPvpInCurrentEpoch","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint8","name":"biome","type":"uint8"},{"indexed":false,"internalType":"uint256","name":"guildId","type":"uint256"},{"indexed":false,"internalType":"uint32","name":"week","type":"uint32"}],"name":"AddBiomeRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"uint256","name":"ts","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"block","type":"uint256"}],"name":"ContractInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"biome","type":"uint8"},{"indexed":false,"internalType":"uint32","name":"week","type":"uint32"}],"name":"FirstPvpEpoch","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint64","name":"version","type":"uint64"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint48","name":"fightId","type":"uint48"},{"indexed":false,"internalType":"uint32","name":"week","type":"uint32"},{"indexed":false,"internalType":"address","name":"hero","type":"address"},{"indexed":false,"internalType":"uint256","name":"heroId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"heroGuildId","type":"uint256"},{"indexed":false,"internalType":"address","name":"opponentHero","type":"address"},{"indexed":false,"internalType":"uint256","name":"opponentHeroId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"opponentGuildId","type":"uint256"}],"name":"PreparePvpFight","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"guildId","type":"uint256"},{"indexed":false,"internalType":"address","name":"hero","type":"address"},{"indexed":false,"internalType":"uint256","name":"heroId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"week","type":"uint256"},{"indexed":false,"internalType":"uint8","name":"biome","type":"uint8"}],"name":"PvpHeroAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"guildId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"week","type":"uint256"},{"indexed":false,"internalType":"uint8","name":"biome","type":"uint8"},{"indexed":false,"internalType":"address","name":"hero","type":"address"},{"indexed":false,"internalType":"uint256","name":"heroId","type":"uint256"},{"indexed":false,"internalType":"bool","name":"manuallyRemoved","type":"bool"}],"name":"PvpHeroRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"address","name":"oldLogic","type":"address"}],"name":"RevisionIncreased","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"adapter","type":"address"}],"name":"SetGuildStakingAdapter","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"level","type":"uint256"}],"name":"SetMinHeroLevel","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"biome","type":"uint8"},{"indexed":false,"internalType":"uint32","name":"week","type":"uint32"},{"indexed":false,"internalType":"uint256","name":"guildBiomeOwnerId","type":"uint256"}],"name":"UpdatePvpEpoch","type":"event"},{"inputs":[],"name":"CONTROLLABLE_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"},{"internalType":"bytes","name":"pvpStrategy","type":"bytes"},{"internalType":"uint8","name":"maxFights","type":"uint8"}],"name":"addPvpHero","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"controller","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"created","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"createdBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentWeek","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"week","type":"uint32"}],"name":"getBiomeGuilds","outputs":[{"internalType":"uint256[]","name":"guildIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"getBiomeOwner","outputs":[{"internalType":"uint256","name":"guildId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"getBiomeTax","outputs":[{"internalType":"uint256","name":"guildId","type":"uint256"},{"internalType":"uint256","name":"taxPercent","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCounterFightId","outputs":[{"internalType":"uint48","name":"","type":"uint48"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockTimestamp","type":"uint256"}],"name":"getCurrentEpochWeek","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"getDominationCounter","outputs":[{"internalType":"uint16","name":"dominationCounter","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"guildId","type":"uint256"},{"internalType":"uint32","name":"week","type":"uint32"}],"name":"getDominationRequest","outputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"address","name":"user","type":"address"},{"internalType":"uint256","name":"index0","type":"uint256"}],"name":"getFightDataByIndex","outputs":[{"components":[{"internalType":"address","name":"fightOpponent","type":"address"},{"internalType":"enum IPvpController.PvpFightStatus","name":"fightStatus","type":"uint8"},{"internalType":"uint32","name":"health","type":"uint32"},{"internalType":"uint32","name":"mana","type":"uint32"},{"internalType":"uint8","name":"countTurns","type":"uint8"}],"internalType":"struct IPvpController.PvpFightData","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"address","name":"user","type":"address"}],"name":"getFightDataLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"uint256","name":"guildId","type":"uint256"}],"name":"getFreeUsers","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"uint256","name":"guildId","type":"uint256"}],"name":"getGuildPoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMinHeroLevel","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"}],"name":"getPvpStrategy","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"}],"name":"getPvpStrategyKind","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"getStartedEpoch","outputs":[{"internalType":"uint32","name":"epochWeek","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"week","type":"uint32"},{"internalType":"address","name":"user","type":"address"}],"name":"getUserState","outputs":[{"components":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"activeFightIndex1","type":"uint32"},{"internalType":"uint32","name":"numHeroesStaked","type":"uint32"},{"internalType":"uint64","name":"guildId","type":"uint64"},{"internalType":"uint8","name":"countFights","type":"uint8"},{"internalType":"uint8","name":"maxFights","type":"uint8"},{"internalType":"uint48","name":"fightId","type":"uint48"}],"internalType":"struct IPvpController.PvpUserState","name":"userState","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"uint256","name":"guildId","type":"uint256"},{"internalType":"uint32","name":"week","type":"uint32"}],"name":"hasPvpHero","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"oldLogic","type":"address"}],"name":"increaseRevision","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"controller_","type":"address"}],"name":"init","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"value_","type":"address"}],"name":"isController","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"value_","type":"address"}],"name":"isGovernance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"},{"internalType":"uint32","name":"epoch","type":"uint32"}],"name":"isHeroStaked","outputs":[{"internalType":"bool","name":"staked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"}],"name":"isHeroStakedCurrently","outputs":[{"internalType":"bool","name":"staked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"forwarder","type":"address"}],"name":"isTrustedForwarder","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"guildId","type":"uint256"}],"name":"onGuildDeletion","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"guildId","type":"uint256"}],"name":"ownedBiome","outputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"prepareFight","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"previousImplementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"refreshBiomeTax","outputs":[{"internalType":"uint256","name":"guildId","type":"uint256"},{"internalType":"uint256","name":"taxPercent","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"uint256","name":"guildId","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"registeredHero","outputs":[{"internalType":"address","name":"hero","type":"address"},{"internalType":"uint256","name":"heroId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"},{"internalType":"uint32","name":"epochWeek","type":"uint32"},{"internalType":"uint256","name":"guildId","type":"uint256"}],"name":"registeredUsers","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"removePvpHero","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"revision","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"selectBiomeForDomination","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"adapter_","type":"address"}],"name":"setGuildStakingAdapter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"level","type":"uint256"}],"name":"setMinHeroLevel","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"maxCountTurns","type":"uint8"}],"name":"startFight","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"biome","type":"uint8"}],"name":"updateEpoch","outputs":[],"stateMutability":"nonpayable","type":"function"}]
Contract Creation Code
60806040523480156200001157600080fd5b507ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff1615906001600160401b03166000811580156200005d5750825b90506000826001600160401b031660011480156200007a5750303b155b90508115801562000089575080155b15620000a85760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b03191660011785558315620000d757845460ff60401b1916680100000000000000001785555b83156200011e57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505061557080620001336000396000f3fe608060405234801561001057600080fd5b506004361061024a5760003560e01c80637cc963801161013c578063c5beb1e0116100be578063c5beb1e014610610578063cb80c82f14610623578063ce24c6e514610636578063ce4a79a914610649578063d2336d2e1461065c578063d49e18be14610664578063dee1f0e414610677578063e22d24af1461068a578063e89acf0614610692578063f2f17ec81461069a578063f77c4791146106bb578063ffa1ad741461057e57600080fd5b80637cc96380146104ae578063859ec734146104b6578063901bccda146104c957806390c928cd146104dc57806390e52dc3146104ef578063936725ec1461057e578063991bb818146105af5780639d0bcca0146105c2578063a399f0d7146105d7578063b2c56c9b146105ea578063b429afeb146105fd57600080fd5b80633349eafa116101d05780633349eafa1461039857806342be39a4146103ab578063454ac5c8146103d15780634593144c146103e457806347ff6204146103ec5780634fac6ccd1461040c578063572b6c051461041f5780635ecd46781461044257806361aa8057146104555780636672009e1461046857806371c2b99c146104885780637a5654d81461049b57600080fd5b806306575c891461024f5780630984b3cb146102715780630abb07c314610299578063150b7a02146102ba5780631518fc25146102f157806319ab453c146103115780631b541a351461032657806325b3b32c146103395780632b529bde1461034c5780632fe7b1b514610371578063325a19f114610390575b600080fd5b6102576106c3565b60405163ffffffff90911681526020015b60405180910390f35b61028461027f3660046149b0565b6106d3565b60408051928352602083019190915201610268565b6102ac6102a73660046149f4565b6106ec565b604051908152602001610268565b6102d86102c8366004614af0565b630a85bd0160e11b949350505050565b6040516001600160e01b03199091168152602001610268565b6103046102ff366004614b5b565b610701565b6040516102689190614b89565b61032461031f366004614bcd565b61070d565b005b6102ac610334366004614bea565b610823565b610324610347366004614c2b565b610838565b61035f61035a366004614c2b565b61084c565b60405160ff9091168152602001610268565b610379610857565b60405165ffffffffffff9091168152602001610268565b6102ac610861565b61035f6103a6366004614c44565b610895565b6103be6103b93660046149b0565b6108a1565b60405161ffff9091168152602001610268565b6102576103df366004614c2b565b6108ac565b6102ac6108b7565b6103ff6103fa366004614c69565b6108e7565b6040516102689190614caf565b61032461041a366004614bcd565b6108fa565b61043261042d366004614bcd565b610a0c565b6040519015158152602001610268565b610432610450366004614d1e565b610a81565b6102ac6104633660046149b0565b610a8e565b61047b610476366004614bea565b610a99565b6040516102689190614d60565b610324610496366004614bcd565b610aa6565b61047b6104a9366004614bea565b610ab7565b6102ac610ac4565b6102576104c43660046149b0565b610ae2565b6103246104d73660046149b0565b610aed565b6102ac6104ea366004614da1565b610b0a565b6105026104fd3660046149f4565b610b21565b6040516102689190600060e08201905060ff8351168252602083015163ffffffff808216602085015280604086015116604085015250506001600160401b03606084015116606083015260ff608084015116608083015260ff60a08401511660a083015265ffffffffffff60c08401511660c083015292915050565b6105a2604051806040016040528060058152602001640312e302e360dc1b81525081565b6040516102689190614e42565b6105a26105bd366004614da1565b610b33565b6105ca610b41565b6040516102689190614e55565b6103246105e5366004614e69565b610b71565b6104326105f8366004614d1e565b610b97565b61043261060b366004614bcd565b610ba4565b61028461061e3660046149b0565b610bc9565b610324610631366004614c2b565b610bd5565b6103246106443660046149b0565b610be6565b610432610657366004614ed4565b610bf3565b610324610c08565b6103246106723660046149b0565b610c26565b610432610685366004614bcd565b610cc0565b610324610d45565b6102ac610d5e565b6106ad6106a8366004614f00565b610d68565b604051610268929190614f48565b6105ca610d85565b60006106ce42610db5565b905090565b6000806106e38342610dd2610e82565b91509150915091565b60006106f88383610eab565b90505b92915050565b60606106f88383610eee565b6000610717610f2a565b805490915060ff600160401b82041615906001600160401b031660008115801561073e5750825b90506000826001600160401b0316600114801561075a5750303b155b905081158015610768575080155b156107865760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156107b057845460ff60401b1916600160401b1785555b6107b986610f4e565b60056107c3610f5f565b600160009081526020919091526040902055831561081b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6000610830848484610f83565b949350505050565b610849610843610d85565b82610fe4565b50565b60006106fb826110d5565b60006106ce6110f6565b60006106ce61089160017f6f55f470bdc9cb5f04223fd822021061668e4dccb43e8727b295106dc9769c8b614f77565b5490565b60006106f8838361112a565b60006106fb82611164565b60006106fb82610db5565b60006106ce61089160017f812a673dfca07956350df10f8a654925f561d7a0da09bdbe79e653939a14d9f1614f77565b6108ef6148ad565b610830848484611195565b33301461094c5760405162461bcd60e51b815260206004820152601b60248201527a24b731b932b0b9b2903932bb34b9b4b7b7103337b93134b23232b760291b60448201526064015b60405180910390fd5b600061096a610891600160008051602061551b833981519152614f77565b610975906001614f8a565b905061099781610994600160008051602061551b833981519152614f77565b55565b6109c68261099460017fbfaaa2fb63266ff27c2da975f5894955056f50419af651a81f6c5060581857e4614f77565b604080518281526001600160a01b03841660208201527ff27e2ef832a4eb8ed8ec553b875eecd44764cda95b1c24170e281539e0a869c891015b60405180910390a15050565b60006001600160a01b03821673d8253782c45a12053594b9deb72d8e8ab2fca54c1480610a5557506001600160a01b0382167352ceba41da235af367bfc0b0ccd3314cb901bb5f145b806106fb57506001600160a01b03821673102f1f556cd9c3d5f820e6920a8931657c5da21b1492915050565b600061083084848461127e565b60006106fb826112bc565b60606108308484846112eb565b610849610ab1610d85565b82611332565b6060610830848484611392565b60006106ce610891600160008051602061551b833981519152614f77565b60006106fb826113d6565b610849610af8611409565b610b00610d85565b8342610dd261142e565b6000610b1885858585611682565b95945050505050565b610b296148dd565b6106f88383611765565b6060610b1885858585611820565b60006106ce61089160017fbfaaa2fb63266ff27c2da975f5894955056f50419af651a81f6c5060581857e4614f77565b610b91610b7c611409565b610b84610d85565b8686868642610dd2611903565b50505050565b6000610830848484612534565b6000610bae610d85565b6001600160a01b0316826001600160a01b0316149050919050565b6000806106e3836125b2565b610849610be0610d85565b8261261c565b6108498142610dd2612671565b60006106f88383610c0342610db5565b61127e565b610c24610c13611409565b610c1b610d85565b42610dd261267c565b565b730caef1783c939632f8c890efb8a8d43935366584638eb687a9610c48611409565b610c50610d85565b6040516001600160e01b031960e085901b1681526001600160a01b0392831660048201529116602482015242604482015260ff8416606482015260840160006040518083038186803b158015610ca557600080fd5b505af4158015610cb9573d6000803e3d6000fd5b5050505050565b6000816001600160a01b0316610cd4610d85565b6001600160a01b0316635aa6e6756040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d11573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d359190614f9d565b6001600160a01b03161492915050565b610c24610d50611409565b610d58610d85565b4261290f565b60006106ce612a16565b600080610d7786868686612a29565b915091505b94509492505050565b60006106ce61089160017f5165972ef41194f06c5007493031d0b927c20741adcb74403b954009fd2c3618614f77565b60006106fb610dc76201518084614fd0565b63ffffffff16612a9a565b600081600003610de457506000919050565b6000610dee612ab3565b9050610dfb836001614f8a565b4340414443423a5a88604051602001610e5898979695949392919097885260609690961b6001600160601b0319166020880152603487019490945260548601929092526074850152609484015260b483015260d482015260f40190565b6040516020818303038152906040528051906020012060001c610e7b9190614fe4565b9392505050565b6000806000610e92868686612aea565b9050610e9d81612f19565b92509250505b935093915050565b6000610eb5610f5f565b63ffffffff84166000908152600391909101602090815260408083206001600160a01b0386168452600201909152902054905092915050565b60606106f8610efb610f5f565b63ffffffff841660009081526003919091016020908152604080832060ff88168452600601909152902061302c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b610f56613039565b6108498161305e565b7f6750db0cf5db3c73c8abbeff54ef9c65daabbed6967cb68f37e0698f5fc7bb0090565b6000806000610fc884610f94610f5f565b63ffffffff80891660009081526003929092016020908152604080842060ff8d168552600101909152909120919061317d16565b9150915081610fd8576000610fda565b805b9695505050505050565b610fed8261319b565b6000610ff7610f5f565b600301600061100542610db5565b63ffffffff1681526020808201929092526040908101600090812085825260058101909352205490915060ff1680156110585760ff811660009081526006830160205260409020611056908461322e565b505b6000611062610f5f565b6000858152600291909101602052604090205460ff1690508015610cb957611088610f5f565b600085815260029190910160205260409020805460ff191690556110aa610f5f565b60ff821660009081526001919091016020526040902080546001600160701b03191690555050505050565b60006110df610f5f565b600092835260020160205250604090205460ff1690565b6000611100610f5f565b600060035b600381111561111657611116614c99565b815260200190815260200160002054905090565b6000611134610f5f565b63ffffffff9290921660009081526003909201602090815260408084209484526005909401905250205460ff1690565b600061116e610f5f565b60ff9290921660009081526001909201602052506040902054600160601b900461ffff1690565b61119d6148ad565b6111a5610f5f565b63ffffffff85166000908152600391909101602090815260408083206001600160a01b038716845260020190915290208054839081106111e7576111e7614ff8565b60009182526020918290206040805160a08101909152910180546001600160a01b03811683529192909190830190600160a01b900460ff16600481111561123057611230614c99565b600481111561124157611241614c99565b81529054600160a81b810463ffffffff9081166020840152600160c81b8204166040830152600160e81b900460ff16606090910152949350505050565b600080611289610f5f565b60030160008463ffffffff1681526020019081526020016000209050610b186112b2868661323a565b600383019061328a565b60006112c6610f5f565b60ff90921660009081526001929092016020525060409020546001600160401b031690565b60606108306112f8610f5f565b63ffffffff851660009081526003919091016020908152604080832060ff89168452600101825280832086845260040190915290206132a2565b61133b826132af565b806001600160a01b031661134d610f5f565b600060028152602001908152602001600020819055507fb55212beea0c27733221a3151de625a0f9a00c6435c0ec42102d42180204eb2c81604051610a009190614e55565b606061083061139f610f5f565b63ffffffff8516600090815260039182016020908152604080832060ff8a168452600101825280832087845290930190522061302c565b60006113e0610f5f565b60ff9290921660009081526001909201602052506040902054600160401b900463ffffffff1690565b600061141433610a0c565b15611426575060131936013560601c90565b503390565b90565b6114378461333b565b6000846001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa158015611477573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061149b9190614f9d565b90508360ff166000036114c657604051637d34ca9360e01b815260ff85166004820152602401610943565b60006114d48288600d6133bb565b50604051636f8eecbb60e11b8152600481018290529091506000906001600160a01b0384169063df1dd97690602401600060405180830381865afa158015611520573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611548919081019061506f565b5050935050505060038160ff161015611574576040516368dfb41160e11b815260040160405180910390fd5b50600061158085610db5565b9050600061158c610f5f565b63ffffffff8316600090815260039190910160209081526040808320868452600581019092529091205490915060ff16156115da57604051637e524e1960e01b815260040160405180910390fd5b6115e5878787612aea565b5060008381526005820160209081526040808320805460ff191660ff8c16908117909155835260068401909152902061161e9084613528565b50604080516001600160a01b038b16815260ff8916602082015290810184905263ffffffff831660608201527f5e1c8323b6cf985eec8241831ea88c82e70a972471b5f5b9afcb5fc46b5eb24a9060800160405180910390a1505050505050505050565b6000610b1861168f610f5f565b63ffffffff861660009081526003919091016020908152604080832060ff8a1684526001019091528120600501906116c7868661323a565b815260200190815260200160002080546116e090615111565b80601f016020809104026020016040519081016040528092919081815260200182805461170c90615111565b80156117595780601f1061172e57610100808354040283529160200191611759565b820191906000526020600020905b81548152906001019060200180831161173c57829003601f168201915b50505050506040015190565b61176d6148dd565b611775610f5f565b63ffffffff9384166000908152600391909101602090815260408083206001600160a01b0395909516835293815290839020835160e081018552905460ff80821683526101008204871693830193909352600160281b810490951693810193909352600160481b84046001600160401b03166060840152600160881b840481166080840152600160901b84041660a083015250600160981b90910465ffffffffffff1660c082015290565b606061182a610f5f565b63ffffffff851660009081526003919091016020908152604080832060ff89168452600101909152812060050190611862858561323a565b8152602001908152602001600020805461187b90615111565b80601f01602080910402602001604051908101604052809291908181526020018280546118a790615111565b80156118f45780601f106118c9576101008083540402835291602001916118f4565b820191906000526020600020905b8154815290600101906020018083116118d757829003601f168201915b50505050509050949350505050565b61190c8761333b565b611914614919565b876001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa158015611952573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119769190614f9d565b81606001906001600160a01b031690816001600160a01b031681525050876001600160a01b031663016dff5d6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156119d1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119f59190614f9d565b81608001906001600160a01b031690816001600160a01b031681525050876001600160a01b0316628e96916040518163ffffffff1660e01b8152600401602060405180830381865afa158015611a4f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a739190614f9d565b6001600160a01b031660a0820152611a8a83610db5565b63ffffffff16602082018190526060820151611aaa918b91906001613534565b60ff1682526101208201526040516331a9108f60e11b8152600481018790526001600160a01b038a81169190891690636352211e90602401602060405180830381865afa158015611aff573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611b239190614f9d565b6001600160a01b031614611b4e57868660405163547208b960e11b8152600401610943929190614f48565b80608001516001600160a01b0316631789b7b1886040518263ffffffff1660e01b8152600401611b7e9190614e55565b602060405180830381865afa158015611b9b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611bbf919061514b565b60ff16600003611be4578660405163adc7fced60e01b81526004016109439190614e55565b876001600160a01b0316632f91ede96040518163ffffffff1660e01b8152600401602060405180830381865afa158015611c22573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611c469190614f9d565b6001600160a01b0316635451da1b88886040518363ffffffff1660e01b8152600401611c73929190614f48565b602060405180830381865afa158015611c90573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611cb49190615168565b15611cd6578686604051634f38de4760e11b8152600401610943929190614f48565b8060a001516001600160a01b031663f16a306688886040518363ffffffff1660e01b8152600401611d08929190614f48565b602060405180830381865afa158015611d25573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611d499190615168565b611d6a57868660405163033aace360e41b8152600401610943929190614f48565b608081015160405163dec38e7760e01b81526001916001600160a01b03169063dec38e7790611d9f908b908b90600401614f48565b602060405180830381865afa158015611dbc573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611de0919061514b565b60ff1603611e0157604051631ed7101960e21b815260040160405180910390fd5b876001600160a01b031663683fedf76040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e3f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e639190614f9d565b6001600160a01b0316639a641e0288886040518363ffffffff1660e01b8152600401611e90929190614f48565b602060405180830381865afa158015611ead573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ed1919061518a565b6001600160401b031615611ef75760405162dc1c0b60e01b815260040160405180910390fd5b608081015160405163ec5316b560e01b81526001916001600160a01b03169063ec5316b590611f2c908b908b90600401614f48565b602060405180830381865afa158015611f49573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f6d91906151a5565b1115611f8c57604051639928185760e01b815260040160405180910390fd5b60a0810151604051631a95890960e31b81526000916001600160a01b03169063d4ac484890611fc1908b908b90600401614f48565b60a060405180830381865afa158015611fde573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061200291906151be565b51905061200d612a16565b8163ffffffff16101561203b57604051634bebee7f60e01b815263ffffffff82166004820152602401610943565b508051612049908484612aea565b506000612054610f5f565b60208381015163ffffffff9081166000908152600393909301825260408084206001600160a01b038f16855280845293819020815160e081018352905460ff80821683526101008204851695830195909552600160281b810490931691810191909152600160481b82046001600160401b03166060820152600160881b820483166080820152600160901b8204831660a0820152600160981b90910465ffffffffffff1660c0820152610140850181905251919250161561213557610140820151516040516321da897960e11b815260ff9091166004820152602401610943565b612144600163ffffffff615255565b63ffffffff168261014001516040015163ffffffff16106121865760208201516040516305eee3b960e11b815263ffffffff9091166004820152602401610943565b815160ff16600090815260018201602052604090206121a5898961323a565b604084015260006121b7886040015190565b905080156121d85760405163d860955b60e01b815260040160405180910390fd5b604080850151600090815260058401602052206121f589826152bf565b50506040808401516101208501516000908152600484016020529190912061221e918d9061363e565b506040830151612232906003840190613528565b506040805160e081018252845160ff16815260006020820152610140850151820151909182019061226490600161537e565b63ffffffff1681526020018461012001516001600160401b03168152602001600060ff1681526020018760ff168152602001600065ffffffffffff168152508260000160008d6001600160a01b03166001600160a01b0316815260200190815260200160002060008201518160000160006101000a81548160ff021916908360ff16021790555060208201518160000160016101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160056101000a81548163ffffffff021916908363ffffffff16021790555060608201518160000160096101000a8154816001600160401b0302191690836001600160401b0316021790555060808201518160000160116101000a81548160ff021916908360ff16021790555060a08201518160000160126101000a81548160ff021916908360ff16021790555060c08201518160000160136101000a81548165ffffffffffff021916908365ffffffffffff1602179055509050507f2521d98b2c88e19cb124cca9f2860404ff4e038f074120bbc3d69e2c751ffc928b8461012001518b8b87602001518860000151604051612459969594939291906001600160a01b0396871681526020810195909552929094166040840152606083015263ffffffff92909216608082015260ff9190911660a082015260c00190565b60405180910390a161247b836060015184600001518486610120015188613654565b61010086015260e08501526001600160a01b031660c084018190526124c057610120830151600090815260038201602052604090206124ba908c613756565b50612527565b6040518060800160405280846020015163ffffffff1681526020018a6001600160a01b031681526020018981526020018460e001518152508361016001819052506125278360000151838d8660c0015187610120015188610100015189610160015161376b565b5050505050505050505050565b60008061253f610f5f565b63ffffffff8416600090815260039190910160209081526040808320878452600581019092529091205490915060ff1680156125a65760ff81166000908152600183016020908152604080832088845260040190915290206125a19087613c19565b610fda565b50600095945050505050565b6000806106e36125c0610f5f565b60ff851660009081526001919091016020908152604091829020825160608101845290546001600160401b0381168252600160401b810463ffffffff1692820192909252600160601b90910461ffff1691810191909152612f19565b612625826132af565b8061262e610f5f565b6001600090815260209182526040908190209290925590518281527fee3ba0763748d6e285026c82cf5f0233531ace4040dce0d5c4434c55bf5cd9399101610a00565b610b91838383612aea565b6126858361333b565b6040805160c081018252600080825260208201819052918101829052606081018290526080810182905260a0810191909152836001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa1580156126f5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127199190614f9d565b6001600160a01b0316606082015261273083610db5565b63ffffffff16602082018190526060820151612750918791906001613534565b60ff168252506000612760610f5f565b60208381015163ffffffff9081166000908152600393909301825260408084206001600160a01b038b16855280845293819020815160e081018352905460ff80821683526101008204851695830195909552600160281b810490931691810191909152600160481b82046001600160401b03166060820152600160881b820483166080820152600160901b820490921660a0830152600160981b900465ffffffffffff1660c082015290915061281581613c2e565b612837836060015184600001518484606001516001600160401b031688613654565b60a086015260808501526001600160a01b03166040840181905261286e57604051633381fb0d60e01b815260040160405180910390fd5b805160ff1660009081526001830160205260408120606083015161289c91906001600160401b03168a613c7c565b90506129058460000151848a876040015186606001516001600160401b03168960a0015160405180608001604052808c6020015163ffffffff16815260200189600001516001600160a01b03168152602001896020015181526020018c6080015181525061376b565b5050505050505050565b6129188261333b565b600061292382610db5565b9050600061292f610f5f565b63ffffffff8381166000908152600392909201602090815260408084206001600160a01b038a16855280835293819020815160e081018352905460ff80821683526101008204861694830194909452600160281b810490941691810191909152600160481b83046001600160401b03166060820152600160881b830482166080820152600160901b830490911660a0820152600160981b90910465ffffffffffff1660c08201529091506129e281613c2e565b805160ff1660009081526001808401602052604090912090612a0d9085908590849086908c90613d91565b50505050505050565b6000612a20610f5f565b60006001611105565b600080600080612a7a85612a3b610f5f565b63ffffffff808b1660009081526003929092016020908152604080842060ff8f16855260010182528084208c8552600401909152909120919061405d16565b915091508115612a8f57925060a083901c9150825b505094509492505050565b60006007612aa9836003614f8a565b6106fb9190614fd0565b60006350877ed6461480612aca5750630235ddd046145b1561142b576040518060208160008060185afa612ae357fe5b5051905090565b6040805160608101825260008082526020820181905291810191909152612b0f610f5f565b60ff8516600090815260019190910160209081526040808320815160608101835290546001600160401b0381168252600160401b810463ffffffff1693820193909352600160601b90920461ffff16908201529150612b6d84610db5565b9050816020015163ffffffff16600003612c3c5763ffffffff8116602083015281612b96610f5f565b60ff8716600081815260019290920160209081526040928390208451815486840151968601516001600160401b039092166001600160601b031990911617600160401b63ffffffff978816021761ffff60601b1916600160601b61ffff909216919091021790558251918252928416928101929092527f1122d4fb8516daedc106927e0acc4baf5f6be5f82112b501be8790859c3d4d9b910160405180910390a1612f11565b816020015163ffffffff168163ffffffff1614612f11576000612c91612c60610f5f565b60208086015163ffffffff1660009081526003929092018152604080832060ff8b168452600601909152902061302c565b90506000612cab87856020015163ffffffff168488614075565b905080600003612cc2575082516001600160401b03165b83516001600160401b03168114612deb576000612cdd610f5f565b60008381526002919091016020526040902054855160ff90911691506001600160401b031615612d3557612d0f610f5f565b85516001600160401b0316600090815260029190910160205260409020805460ff191690555b60ff811615612dbc576000612d48610f5f565b60ff8316600090815260019190910160205260408120805467ffffffffffffffff19166001600160401b039390931692909217909155612d86610f5f565b60ff8316600090815260019190910160205260409020805461ffff92909216600160601b0261ffff60601b199092169190911790555b87612dc5610f5f565b600084815260029190910160205260409020805460ff191660ff92909216919091179055505b6040518060600160405280826001600160401b031681526020018463ffffffff16815260200185600001516001600160401b031683148015612e2c57508215155b612e37576000612e55565b6020860151612e469086615255565b8660400151612e55919061539b565b61ffff169052935083612e66610f5f565b60ff8916600081815260019290920160209081526040928390208451815486840151968601516001600160401b039092166001600160601b031990911617600160401b63ffffffff978816021761ffff60601b1916600160601b61ffff9092169190910217905582519182529286169281019290925281018290527f1ffb10ddeed1be3e5ce0225fdd14d2df110d52b970118a7f9d58de2ecce3c0629060600160405180910390a150505b509392505050565b80516001600160401b031660008115612f34576103e8612f37565b60005b90508115613027576000612f49610f5f565b60026000908152602091909152604090205490506001600160a01b03811615613025576040516306d71c2560e51b8152600481018490526000906001600160a01b0383169063dae384a090602401602060405180830381865afa158015612fb4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612fd891906151a5565b9050670de0b6b3a7640000612ff582670de0b6b3a76400006142ed565b6130036103e8612710614f77565b61300d91906153b6565b6130179190614fd0565b6130219084614f8a565b9250505b505b915091565b60606000610e7b83614303565b61304161435f565b610c2457604051631afcd79f60e31b815260040160405180910390fd5b6001600160a01b0381166130a65760405162461bcd60e51b815260206004820152600f60248201526e2d32b9379031b7b73a3937b63632b960891b6044820152606401610943565b6130d58161099460017f5165972ef41194f06c5007493031d0b927c20741adcb74403b954009fd2c3618614f77565b6131044261099460017f6f55f470bdc9cb5f04223fd822021061668e4dccb43e8727b295106dc9769c8b614f77565b6131334361099460017f812a673dfca07956350df10f8a654925f561d7a0da09bdbe79e653939a14d9f1614f77565b604080516001600160a01b0383168152426020820152438183015290517f1a2dd071001ebf6e03174e3df5b305795a4ad5d41d8fdb9ba41dbbe2367134269181900360600190a150565b600080808061318c8686614379565b909450925050505b9250929050565b336001600160a01b0316816001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa1580156131e3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906132079190614f9d565b6001600160a01b031614610849576040516302619f2b60e51b815260040160405180910390fd5b60006106f883836143bb565b60006001600160401b0382111561326757604051633995b34160e01b815260048101839052602401610943565b5067ffffffffffffffff60a01b60a09190911b166001600160a01b039091161790565b600081815260018301602052604081205415156106f8565b60606000610e7b836144b5565b604051631430d62960e21b81526001600160a01b038216906350c358a4906132db903390600401614e55565b602060405180830381865afa1580156132f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061331c9190615168565b610849573360405163451cea1760e11b81526004016109439190614e55565b806001600160a01b03166318d928316040518163ffffffff1660e01b8152600401602060405180830381865afa158015613379573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061339d9190615168565b1561084957604051635e1633d360e11b815260040160405180910390fd5b600080846001600160a01b0316633b4c9891856040518263ffffffff1660e01b81526004016133ea9190614e55565b602060405180830381865afa158015613407573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061342b91906151a5565b6040516373cf874b60e11b81529092506001600160a01b0386169063e79f0e969061345a908790600401614e55565b602060405180830381865afa158015613477573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061349b91906151a5565b9050816000036134be57604051632f6c853960e11b815260040160405180910390fd5b6134ca600060026154a9565b81161515806134f6575082600d8111156134e6576134e6614c99565b6134f19060026154a9565b811615155b610ea35782600d81111561350c5761350c614c99565b604051630d816ed160e01b815260040161094391815260200190565b60006106f883836144c0565b600080846001600160a01b0316633b4c9891876040518263ffffffff1660e01b81526004016135639190614e55565b602060405180830381865afa158015613580573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906135a491906151a5565b91508280156135b1575081155b156135cf57604051632f6c853960e11b815260040160405180910390fd5b811561360d576135dd610f5f565b63ffffffff851660009081526003919091016020908152604080832085845260050190915290205460ff16613610565b60005b9050828015613620575060ff8116155b15610d7c57604051632e8e6b7560e11b815260040160405180910390fd5b6000610830846001600160a01b0385168461450f565b600080600080886001600160a01b031663df1dd976876040518263ffffffff1660e01b815260040161368891815260200190565b600060405180830381865afa1580156136a5573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526136cd919081019061506f565b5050935050505060038160ff161061374a5760ff88166000908152600188016020908152604080832060068b0190925290912061370e908b90838a8a61452c565b92508215613748576137218184886146eb565b909550935060008490036137485760405163d92e233d60e01b815260040160405180910390fd5b505b50955095509592505050565b60006106f8836001600160a01b0384166144c0565b6001600160a01b0385811660009081526002880160209081526040808320815160a08101835289861681526001818501818152938201869052606082018690526080820186905282549081018355918552929093208251930180546001600160a01b0319811694909516938417815590519193909283916001600160a81b03191617600160a01b83600481111561380457613804614c99565b0217905550604082810151825460608086015160809687015160ff16600160e81b0260ff60e81b1963ffffffff928316600160c81b0263ffffffff60c81b1993909616600160a81b029290921667ffffffffffffffff60a81b19909416939093179390931792909216179092556001600160a01b03878116600090815260028b016020908152838220845160a0810186528c85168152600181840181815296820185905296810184905296870183905280549586018155825290208451930180546001600160a01b031981169490921693841781559151919283916001600160a81b03191617600160a01b83600481111561390157613901614c99565b021790555060408201518154606084015160809094015167ffffffffffffffff60a81b19909116600160a81b63ffffffff9384160263ffffffff60c81b191617600160c81b92909416919091029290921760ff60e81b1916600160e81b60ff90931692909202919091179055600061397761476f565b9050866002016000876001600160a01b03166001600160a01b0316815260200190815260200160002080549050876000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060000160016101000a81548163ffffffff021916908363ffffffff16021790555080876000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060000160136101000a81548165ffffffffffff021916908365ffffffffffff160217905550866002016000866001600160a01b03166001600160a01b0316815260200190815260200160002080549050876000016000876001600160a01b03166001600160a01b0316815260200190815260200160002060000160016101000a81548163ffffffff021916908363ffffffff16021790555080876000016000876001600160a01b03166001600160a01b0316815260200190815260200160002060000160136101000a81548165ffffffffffff021916908365ffffffffffff160217905550613b3a868860010160008b60ff1660ff16815260200190815260200160002060030160008781526020019081526020016000206147be90919063ffffffff16565b5060ff8816600090815260018801602090815260408083208684526003019091529020613b6790866147be565b50600080613b7c84606001519060a082901c90565b85516020808801516040808a0151815165ffffffffffff8b16815263ffffffff909516938501939093526001600160a01b03918216908401526060830191909152608082018a9052831660a082015260c0810182905260e0810188905291935091507fd7da27827582ad8f7f3f4fc8a20f7be83108736c88b28aa3b41dccf6ad6e877990610100015b60405180910390a150505050505050505050565b60006106f8836001600160a01b0384166147d3565b805160ff16600003613c53576040516315e0349160e11b815260040160405180910390fd5b602081015163ffffffff1615610849576040516366fab05760e01b815260040160405180910390fd5b60408051606080820183526000808352602083015291810191909152600083815260048501602052604081208190613cb4908561405d565b91509150600082613cc6576000613cc8565b815b90508060a081901c6020808701919091526001600160a01b039091168552600082815260058901909152604090208054613d0190615111565b80601f0160208091040260200160405190810160405280929190818152602001828054613d2d90615111565b8015613d7a5780601f10613d4f57610100808354040283529160200191613d7a565b820191906000526020600020905b815481529060010190602001808311613d5d57829003601f168201915b505050505084604001819052505050509392505050565b60608301516001600160401b031660009081526004850160205260408120819081908190613dbf908761405d565b915091508115613e0b5760608701516001600160401b031660009081526004890160205260409020613df190876147df565b50613dff60038a018261322e565b50925060a083901c9150825b60608701516001600160401b031660009081526003890160205260409020613e3390876147be565b506040518060e00160405280600060ff168152602001600063ffffffff168152602001886040015163ffffffff16815260200160006001600160401b03168152602001600060ff168152602001600060ff168152602001600065ffffffffffff16815250896000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060008201518160000160006101000a81548160ff021916908360ff16021790555060208201518160000160016101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160056101000a81548163ffffffff021916908363ffffffff16021790555060608201518160000160096101000a8154816001600160401b0302191690836001600160401b0316021790555060808201518160000160116101000a81548160ff021916908360ff16021790555060a08201518160000160126101000a81548160ff021916908360ff16021790555060c08201518160000160136101000a81548165ffffffffffff021916908365ffffffffffff1602179055509050507f43123e2ba4590155d8a1fddade2bb91cc46a6ce2a49d8fba12fdeae963bc7be88688606001518c8a6000015188888b604051613c0597969594939291906001600160a01b0397881681526001600160401b0396909616602087015263ffffffff94909416604086015260ff929092166060850152909316608083015260a082019290925290151560c082015260e00190565b600080808061318c866001600160a01b038716614379565b815160009081816001600160401b0381111561409357614093614a2d565b6040519080825280602002602001820160405280156140bc578160200160208202803683370190505b50905060008060006140cc610f5f565b60008a81526003919091016020908152604080832060ff8e168452600101909152812091505b858110156141eb5760008061412c8b848151811061411257614112614ff8565b60200260200101518560000161317d90919063ffffffff16565b9150915081801561413c57508015155b156141e1578087848151811061415457614154614ff8565b602090810291909101015285158061418e57508087614174600189614f77565b8151811061418457614184614ff8565b6020026020010151105b156141a95761419e836001614f8a565b9550600194506141e1565b80876141b6600189614f77565b815181106141c6576141c6614ff8565b6020026020010151036141e157846141dd816154b5565b9550505b50506001016140f2565b5082156142e0576000826001146142185761421361420a600185614f77565b8963ffffffff16565b61421b565b60005b9050806000036142505788614231600186614f77565b8151811061424157614241614ff8565b602002602001015196506142de565b835b868110156142dc5785614266600187614f77565b8151811061427657614276614ff8565b602002602001015186828151811061429057614290614ff8565b6020026020010151036142d457816001036142c6578981815181106142b7576142b7614ff8565b602002602001015197506142dc565b816142d0816154ce565b9250505b600101614252565b505b505b5050505050949350505050565b60008183106142fc57816106f8565b5090919050565b60608160000180548060200260200160405190810160405280929190818152602001828054801561435357602002820191906000526020600020905b81548152602001906001019080831161433f575b50505050509050919050565b6000614369610f2a565b54600160401b900460ff16919050565b60008181526002830160205260408120548190806143a85761439b85856147d3565b9250600091506131949050565b600192509050613194565b509250929050565b600081815260018301602052604081205480156144a45760006143df600183614f77565b85549091506000906143f390600190614f77565b905080821461445857600086600001828154811061441357614413614ff8565b906000526020600020015490508087600001848154811061443657614436614ff8565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080614469576144696154e5565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506106fb565b60009150506106fb565b5092915050565b60606106fb8261302c565b6000818152600183016020526040812054614507575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556106fb565b5060006106fb565b600082815260028401602052604081208290556108308484613528565b600080614538866147f4565b905060018111156146e157600061455c614553600184614f77565b8563ffffffff16565b9050805b600061456c89836147fe565b604051636f8eecbb60e11b8152600481018290529091506000906001600160a01b038c169063df1dd97690602401600060405180830381865afa1580156145b7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526145df919081019061506f565b5050935050505060038160ff16101580156146115750600082815260038a016020526040902061460e906147f4565b15155b801561461d5750818814155b80156146965750604051630562452b60e21b815260048101899052602481018390526001600160a01b038c169063158914ac90604401602060405180830381865afa158015614670573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906146949190615168565b155b156146a3575093506146de565b846146af846001614f8a565b146146c4576146bf836001614f8a565b6146c7565b60005b92508383036146d75750506146de565b5050614560565b50505b5095945050505050565b60008281526003840160205260408120819081614707826147f4565b905080156147655760008160011461473557614730614727600184614f77565b8763ffffffff16565b614738565b60005b905061474483826147fe565b600088815260048a0160205260409020909550614761908661480a565b9350505b5050935093915050565b6000614779610f5f565b6003600090815260209190915260409020546147969060016154fb565b90508065ffffffffffff166147a9610f5f565b60036000908152602091909152604090205590565b60006106f8836001600160a01b0384166143bb565b60006106f8838361328a565b60006106f8836001600160a01b03841661481f565b60006106fb825490565b60006106f8838361483c565b60006106f8836001600160a01b038416614866565b600081815260028301602052604081208190556106f8838361322e565b600082600001828154811061485357614853614ff8565b9060005260206000200154905092915050565b60008181526002830160205260408120548015801561488c575061488a84846147d3565b155b156106f85760405163015ab34360e11b815260048101849052602401610943565b6040805160a081019091526000808252602082019081526000602082018190526040820181905260609091015290565b6040805160e081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081019190915290565b6040805161018081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052610100810182905261012081019190915261014081016149776148dd565b81526040805160808101825260008082526020828101829052928201819052606082015291015290565b60ff8116811461084957600080fd5b6000602082840312156149c257600080fd5b81356106f8816149a1565b63ffffffff8116811461084957600080fd5b6001600160a01b038116811461084957600080fd5b60008060408385031215614a0757600080fd5b8235614a12816149cd565b91506020830135614a22816149df565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715614a6b57614a6b614a2d565b604052919050565b60006001600160401b03821115614a8c57614a8c614a2d565b50601f01601f191660200190565b600082601f830112614aab57600080fd5b8135614abe614ab982614a73565b614a43565b818152846020838601011115614ad357600080fd5b816020850160208301376000918101602001919091529392505050565b60008060008060808587031215614b0657600080fd5b8435614b11816149df565b93506020850135614b21816149df565b92506040850135915060608501356001600160401b03811115614b4357600080fd5b614b4f87828801614a9a565b91505092959194509250565b60008060408385031215614b6e57600080fd5b8235614b79816149a1565b91506020830135614a22816149cd565b6020808252825182820181905260009190848201906040850190845b81811015614bc157835183529284019291840191600101614ba5565b50909695505050505050565b600060208284031215614bdf57600080fd5b81356106f8816149df565b600080600060608486031215614bff57600080fd5b8335614c0a816149a1565b92506020840135614c1a816149cd565b929592945050506040919091013590565b600060208284031215614c3d57600080fd5b5035919050565b60008060408385031215614c5757600080fd5b823591506020830135614a22816149cd565b600080600060608486031215614c7e57600080fd5b8335614c89816149cd565b92506020840135614c1a816149df565b634e487b7160e01b600052602160045260246000fd5b81516001600160a01b03168152602082015160a082019060058110614ce457634e487b7160e01b600052602160045260246000fd5b80602084015250604083015163ffffffff8082166040850152806060860151166060850152505060ff608084015116608083015292915050565b600080600060608486031215614d3357600080fd5b8335614d3e816149df565b9250602084013591506040840135614d55816149cd565b809150509250925092565b6020808252825182820181905260009190848201906040850190845b81811015614bc15783516001600160a01b031683529284019291840191600101614d7c565b60008060008060808587031215614db757600080fd5b8435614dc2816149a1565b93506020850135614dd2816149cd565b92506040850135614de2816149df565b9396929550929360600135925050565b60005b83811015614e0d578181015183820152602001614df5565b50506000910152565b60008151808452614e2e816020860160208601614df2565b601f01601f19169290920160200192915050565b6020815260006106f86020830184614e16565b6001600160a01b0391909116815260200190565b60008060008060808587031215614e7f57600080fd5b8435614e8a816149df565b93506020850135925060408501356001600160401b03811115614eac57600080fd5b614eb887828801614a9a565b9250506060850135614ec9816149a1565b939692955090935050565b60008060408385031215614ee757600080fd5b8235614ef2816149df565b946020939093013593505050565b60008060008060808587031215614f1657600080fd5b8435614f21816149a1565b93506020850135614f31816149cd565b9250604085013591506060850135614ec9816149df565b6001600160a01b03929092168252602082015260400190565b634e487b7160e01b600052601160045260246000fd5b818103818111156106fb576106fb614f61565b808201808211156106fb576106fb614f61565b600060208284031215614faf57600080fd5b81516106f8816149df565b634e487b7160e01b600052601260045260246000fd5b600082614fdf57614fdf614fba565b500490565b600082614ff357614ff3614fba565b500690565b634e487b7160e01b600052603260045260246000fd5b600082601f83011261501f57600080fd5b815161502d614ab982614a73565b81815284602083860101111561504257600080fd5b610830826020830160208701614df2565b80516001600160401b038116811461506a57600080fd5b919050565b60008060008060008060c0878903121561508857600080fd5b86516001600160401b038082111561509f57600080fd5b6150ab8a838b0161500e565b975060208901519150808211156150c157600080fd5b506150ce89828a0161500e565b95505060408701516150df816149df565b60608801519094506150f0816149a1565b92506150fe60808801615053565b915060a087015190509295509295509295565b600181811c9082168061512557607f821691505b60208210810361514557634e487b7160e01b600052602260045260246000fd5b50919050565b60006020828403121561515d57600080fd5b81516106f8816149a1565b60006020828403121561517a57600080fd5b815180151581146106f857600080fd5b60006020828403121561519c57600080fd5b6106f882615053565b6000602082840312156151b757600080fd5b5051919050565b600060a082840312156151d057600080fd5b60405160a081018181106001600160401b03821117156151f2576151f2614a2d565b6040528251615200816149cd565b81526020830151615210816149cd565b60208201526040830151615223816149cd565b60408201526060830151615236816149cd565b60608201526080830151615249816149cd565b60808201529392505050565b63ffffffff8281168282160390808211156144ae576144ae614f61565b601f8211156152ba576000816000526020600020601f850160051c8101602086101561529b5750805b601f850160051c820191505b8181101561081b578281556001016152a7565b505050565b81516001600160401b038111156152d8576152d8614a2d565b6152ec816152e68454615111565b84615272565b602080601f83116001811461532157600084156153095750858301515b600019600386901b1c1916600185901b17855561081b565b600085815260208120601f198616915b8281101561535057888601518255948401946001909101908401615331565b508582101561536e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b63ffffffff8181168382160190808211156144ae576144ae614f61565b61ffff8181168382160190808211156144ae576144ae614f61565b80820281158282048414176106fb576106fb614f61565b600181815b808511156143b35781600019048211156153ee576153ee614f61565b808516156153fb57918102915b93841c93908002906153d2565b600082615417575060016106fb565b81615424575060006106fb565b816001811461543a576002811461544457615460565b60019150506106fb565b60ff84111561545557615455614f61565b50506001821b6106fb565b5060208310610133831016604e8410600b8410161715615483575081810a6106fb565b61548d83836153cd565b80600019048211156154a1576154a1614f61565b029392505050565b60006106f88383615408565b6000600182016154c7576154c7614f61565b5060010190565b6000816154dd576154dd614f61565b506000190190565b634e487b7160e01b600052603160045260246000fd5b65ffffffffffff8181168382160190808211156144ae576144ae614f6156fe22573091f17911fb166032a3d9e0554aa73d31b7b7ddea4a4dd2995650af84bda2646970667358221220ec077ac823acdea895f5fb7d2e6b705bb9b790cb3012f4d26484cd13000f9d2e64736f6c63430008170033
Deployed Bytecode
0x608060405234801561001057600080fd5b506004361061024a5760003560e01c80637cc963801161013c578063c5beb1e0116100be578063c5beb1e014610610578063cb80c82f14610623578063ce24c6e514610636578063ce4a79a914610649578063d2336d2e1461065c578063d49e18be14610664578063dee1f0e414610677578063e22d24af1461068a578063e89acf0614610692578063f2f17ec81461069a578063f77c4791146106bb578063ffa1ad741461057e57600080fd5b80637cc96380146104ae578063859ec734146104b6578063901bccda146104c957806390c928cd146104dc57806390e52dc3146104ef578063936725ec1461057e578063991bb818146105af5780639d0bcca0146105c2578063a399f0d7146105d7578063b2c56c9b146105ea578063b429afeb146105fd57600080fd5b80633349eafa116101d05780633349eafa1461039857806342be39a4146103ab578063454ac5c8146103d15780634593144c146103e457806347ff6204146103ec5780634fac6ccd1461040c578063572b6c051461041f5780635ecd46781461044257806361aa8057146104555780636672009e1461046857806371c2b99c146104885780637a5654d81461049b57600080fd5b806306575c891461024f5780630984b3cb146102715780630abb07c314610299578063150b7a02146102ba5780631518fc25146102f157806319ab453c146103115780631b541a351461032657806325b3b32c146103395780632b529bde1461034c5780632fe7b1b514610371578063325a19f114610390575b600080fd5b6102576106c3565b60405163ffffffff90911681526020015b60405180910390f35b61028461027f3660046149b0565b6106d3565b60408051928352602083019190915201610268565b6102ac6102a73660046149f4565b6106ec565b604051908152602001610268565b6102d86102c8366004614af0565b630a85bd0160e11b949350505050565b6040516001600160e01b03199091168152602001610268565b6103046102ff366004614b5b565b610701565b6040516102689190614b89565b61032461031f366004614bcd565b61070d565b005b6102ac610334366004614bea565b610823565b610324610347366004614c2b565b610838565b61035f61035a366004614c2b565b61084c565b60405160ff9091168152602001610268565b610379610857565b60405165ffffffffffff9091168152602001610268565b6102ac610861565b61035f6103a6366004614c44565b610895565b6103be6103b93660046149b0565b6108a1565b60405161ffff9091168152602001610268565b6102576103df366004614c2b565b6108ac565b6102ac6108b7565b6103ff6103fa366004614c69565b6108e7565b6040516102689190614caf565b61032461041a366004614bcd565b6108fa565b61043261042d366004614bcd565b610a0c565b6040519015158152602001610268565b610432610450366004614d1e565b610a81565b6102ac6104633660046149b0565b610a8e565b61047b610476366004614bea565b610a99565b6040516102689190614d60565b610324610496366004614bcd565b610aa6565b61047b6104a9366004614bea565b610ab7565b6102ac610ac4565b6102576104c43660046149b0565b610ae2565b6103246104d73660046149b0565b610aed565b6102ac6104ea366004614da1565b610b0a565b6105026104fd3660046149f4565b610b21565b6040516102689190600060e08201905060ff8351168252602083015163ffffffff808216602085015280604086015116604085015250506001600160401b03606084015116606083015260ff608084015116608083015260ff60a08401511660a083015265ffffffffffff60c08401511660c083015292915050565b6105a2604051806040016040528060058152602001640312e302e360dc1b81525081565b6040516102689190614e42565b6105a26105bd366004614da1565b610b33565b6105ca610b41565b6040516102689190614e55565b6103246105e5366004614e69565b610b71565b6104326105f8366004614d1e565b610b97565b61043261060b366004614bcd565b610ba4565b61028461061e3660046149b0565b610bc9565b610324610631366004614c2b565b610bd5565b6103246106443660046149b0565b610be6565b610432610657366004614ed4565b610bf3565b610324610c08565b6103246106723660046149b0565b610c26565b610432610685366004614bcd565b610cc0565b610324610d45565b6102ac610d5e565b6106ad6106a8366004614f00565b610d68565b604051610268929190614f48565b6105ca610d85565b60006106ce42610db5565b905090565b6000806106e38342610dd2610e82565b91509150915091565b60006106f88383610eab565b90505b92915050565b60606106f88383610eee565b6000610717610f2a565b805490915060ff600160401b82041615906001600160401b031660008115801561073e5750825b90506000826001600160401b0316600114801561075a5750303b155b905081158015610768575080155b156107865760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156107b057845460ff60401b1916600160401b1785555b6107b986610f4e565b60056107c3610f5f565b600160009081526020919091526040902055831561081b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6000610830848484610f83565b949350505050565b610849610843610d85565b82610fe4565b50565b60006106fb826110d5565b60006106ce6110f6565b60006106ce61089160017f6f55f470bdc9cb5f04223fd822021061668e4dccb43e8727b295106dc9769c8b614f77565b5490565b60006106f8838361112a565b60006106fb82611164565b60006106fb82610db5565b60006106ce61089160017f812a673dfca07956350df10f8a654925f561d7a0da09bdbe79e653939a14d9f1614f77565b6108ef6148ad565b610830848484611195565b33301461094c5760405162461bcd60e51b815260206004820152601b60248201527a24b731b932b0b9b2903932bb34b9b4b7b7103337b93134b23232b760291b60448201526064015b60405180910390fd5b600061096a610891600160008051602061551b833981519152614f77565b610975906001614f8a565b905061099781610994600160008051602061551b833981519152614f77565b55565b6109c68261099460017fbfaaa2fb63266ff27c2da975f5894955056f50419af651a81f6c5060581857e4614f77565b604080518281526001600160a01b03841660208201527ff27e2ef832a4eb8ed8ec553b875eecd44764cda95b1c24170e281539e0a869c891015b60405180910390a15050565b60006001600160a01b03821673d8253782c45a12053594b9deb72d8e8ab2fca54c1480610a5557506001600160a01b0382167352ceba41da235af367bfc0b0ccd3314cb901bb5f145b806106fb57506001600160a01b03821673102f1f556cd9c3d5f820e6920a8931657c5da21b1492915050565b600061083084848461127e565b60006106fb826112bc565b60606108308484846112eb565b610849610ab1610d85565b82611332565b6060610830848484611392565b60006106ce610891600160008051602061551b833981519152614f77565b60006106fb826113d6565b610849610af8611409565b610b00610d85565b8342610dd261142e565b6000610b1885858585611682565b95945050505050565b610b296148dd565b6106f88383611765565b6060610b1885858585611820565b60006106ce61089160017fbfaaa2fb63266ff27c2da975f5894955056f50419af651a81f6c5060581857e4614f77565b610b91610b7c611409565b610b84610d85565b8686868642610dd2611903565b50505050565b6000610830848484612534565b6000610bae610d85565b6001600160a01b0316826001600160a01b0316149050919050565b6000806106e3836125b2565b610849610be0610d85565b8261261c565b6108498142610dd2612671565b60006106f88383610c0342610db5565b61127e565b610c24610c13611409565b610c1b610d85565b42610dd261267c565b565b730caef1783c939632f8c890efb8a8d43935366584638eb687a9610c48611409565b610c50610d85565b6040516001600160e01b031960e085901b1681526001600160a01b0392831660048201529116602482015242604482015260ff8416606482015260840160006040518083038186803b158015610ca557600080fd5b505af4158015610cb9573d6000803e3d6000fd5b5050505050565b6000816001600160a01b0316610cd4610d85565b6001600160a01b0316635aa6e6756040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d11573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d359190614f9d565b6001600160a01b03161492915050565b610c24610d50611409565b610d58610d85565b4261290f565b60006106ce612a16565b600080610d7786868686612a29565b915091505b94509492505050565b60006106ce61089160017f5165972ef41194f06c5007493031d0b927c20741adcb74403b954009fd2c3618614f77565b60006106fb610dc76201518084614fd0565b63ffffffff16612a9a565b600081600003610de457506000919050565b6000610dee612ab3565b9050610dfb836001614f8a565b4340414443423a5a88604051602001610e5898979695949392919097885260609690961b6001600160601b0319166020880152603487019490945260548601929092526074850152609484015260b483015260d482015260f40190565b6040516020818303038152906040528051906020012060001c610e7b9190614fe4565b9392505050565b6000806000610e92868686612aea565b9050610e9d81612f19565b92509250505b935093915050565b6000610eb5610f5f565b63ffffffff84166000908152600391909101602090815260408083206001600160a01b0386168452600201909152902054905092915050565b60606106f8610efb610f5f565b63ffffffff841660009081526003919091016020908152604080832060ff88168452600601909152902061302c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b610f56613039565b6108498161305e565b7f6750db0cf5db3c73c8abbeff54ef9c65daabbed6967cb68f37e0698f5fc7bb0090565b6000806000610fc884610f94610f5f565b63ffffffff80891660009081526003929092016020908152604080842060ff8d168552600101909152909120919061317d16565b9150915081610fd8576000610fda565b805b9695505050505050565b610fed8261319b565b6000610ff7610f5f565b600301600061100542610db5565b63ffffffff1681526020808201929092526040908101600090812085825260058101909352205490915060ff1680156110585760ff811660009081526006830160205260409020611056908461322e565b505b6000611062610f5f565b6000858152600291909101602052604090205460ff1690508015610cb957611088610f5f565b600085815260029190910160205260409020805460ff191690556110aa610f5f565b60ff821660009081526001919091016020526040902080546001600160701b03191690555050505050565b60006110df610f5f565b600092835260020160205250604090205460ff1690565b6000611100610f5f565b600060035b600381111561111657611116614c99565b815260200190815260200160002054905090565b6000611134610f5f565b63ffffffff9290921660009081526003909201602090815260408084209484526005909401905250205460ff1690565b600061116e610f5f565b60ff9290921660009081526001909201602052506040902054600160601b900461ffff1690565b61119d6148ad565b6111a5610f5f565b63ffffffff85166000908152600391909101602090815260408083206001600160a01b038716845260020190915290208054839081106111e7576111e7614ff8565b60009182526020918290206040805160a08101909152910180546001600160a01b03811683529192909190830190600160a01b900460ff16600481111561123057611230614c99565b600481111561124157611241614c99565b81529054600160a81b810463ffffffff9081166020840152600160c81b8204166040830152600160e81b900460ff16606090910152949350505050565b600080611289610f5f565b60030160008463ffffffff1681526020019081526020016000209050610b186112b2868661323a565b600383019061328a565b60006112c6610f5f565b60ff90921660009081526001929092016020525060409020546001600160401b031690565b60606108306112f8610f5f565b63ffffffff851660009081526003919091016020908152604080832060ff89168452600101825280832086845260040190915290206132a2565b61133b826132af565b806001600160a01b031661134d610f5f565b600060028152602001908152602001600020819055507fb55212beea0c27733221a3151de625a0f9a00c6435c0ec42102d42180204eb2c81604051610a009190614e55565b606061083061139f610f5f565b63ffffffff8516600090815260039182016020908152604080832060ff8a168452600101825280832087845290930190522061302c565b60006113e0610f5f565b60ff9290921660009081526001909201602052506040902054600160401b900463ffffffff1690565b600061141433610a0c565b15611426575060131936013560601c90565b503390565b90565b6114378461333b565b6000846001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa158015611477573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061149b9190614f9d565b90508360ff166000036114c657604051637d34ca9360e01b815260ff85166004820152602401610943565b60006114d48288600d6133bb565b50604051636f8eecbb60e11b8152600481018290529091506000906001600160a01b0384169063df1dd97690602401600060405180830381865afa158015611520573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611548919081019061506f565b5050935050505060038160ff161015611574576040516368dfb41160e11b815260040160405180910390fd5b50600061158085610db5565b9050600061158c610f5f565b63ffffffff8316600090815260039190910160209081526040808320868452600581019092529091205490915060ff16156115da57604051637e524e1960e01b815260040160405180910390fd5b6115e5878787612aea565b5060008381526005820160209081526040808320805460ff191660ff8c16908117909155835260068401909152902061161e9084613528565b50604080516001600160a01b038b16815260ff8916602082015290810184905263ffffffff831660608201527f5e1c8323b6cf985eec8241831ea88c82e70a972471b5f5b9afcb5fc46b5eb24a9060800160405180910390a1505050505050505050565b6000610b1861168f610f5f565b63ffffffff861660009081526003919091016020908152604080832060ff8a1684526001019091528120600501906116c7868661323a565b815260200190815260200160002080546116e090615111565b80601f016020809104026020016040519081016040528092919081815260200182805461170c90615111565b80156117595780601f1061172e57610100808354040283529160200191611759565b820191906000526020600020905b81548152906001019060200180831161173c57829003601f168201915b50505050506040015190565b61176d6148dd565b611775610f5f565b63ffffffff9384166000908152600391909101602090815260408083206001600160a01b0395909516835293815290839020835160e081018552905460ff80821683526101008204871693830193909352600160281b810490951693810193909352600160481b84046001600160401b03166060840152600160881b840481166080840152600160901b84041660a083015250600160981b90910465ffffffffffff1660c082015290565b606061182a610f5f565b63ffffffff851660009081526003919091016020908152604080832060ff89168452600101909152812060050190611862858561323a565b8152602001908152602001600020805461187b90615111565b80601f01602080910402602001604051908101604052809291908181526020018280546118a790615111565b80156118f45780601f106118c9576101008083540402835291602001916118f4565b820191906000526020600020905b8154815290600101906020018083116118d757829003601f168201915b50505050509050949350505050565b61190c8761333b565b611914614919565b876001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa158015611952573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119769190614f9d565b81606001906001600160a01b031690816001600160a01b031681525050876001600160a01b031663016dff5d6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156119d1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119f59190614f9d565b81608001906001600160a01b031690816001600160a01b031681525050876001600160a01b0316628e96916040518163ffffffff1660e01b8152600401602060405180830381865afa158015611a4f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a739190614f9d565b6001600160a01b031660a0820152611a8a83610db5565b63ffffffff16602082018190526060820151611aaa918b91906001613534565b60ff1682526101208201526040516331a9108f60e11b8152600481018790526001600160a01b038a81169190891690636352211e90602401602060405180830381865afa158015611aff573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611b239190614f9d565b6001600160a01b031614611b4e57868660405163547208b960e11b8152600401610943929190614f48565b80608001516001600160a01b0316631789b7b1886040518263ffffffff1660e01b8152600401611b7e9190614e55565b602060405180830381865afa158015611b9b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611bbf919061514b565b60ff16600003611be4578660405163adc7fced60e01b81526004016109439190614e55565b876001600160a01b0316632f91ede96040518163ffffffff1660e01b8152600401602060405180830381865afa158015611c22573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611c469190614f9d565b6001600160a01b0316635451da1b88886040518363ffffffff1660e01b8152600401611c73929190614f48565b602060405180830381865afa158015611c90573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611cb49190615168565b15611cd6578686604051634f38de4760e11b8152600401610943929190614f48565b8060a001516001600160a01b031663f16a306688886040518363ffffffff1660e01b8152600401611d08929190614f48565b602060405180830381865afa158015611d25573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611d499190615168565b611d6a57868660405163033aace360e41b8152600401610943929190614f48565b608081015160405163dec38e7760e01b81526001916001600160a01b03169063dec38e7790611d9f908b908b90600401614f48565b602060405180830381865afa158015611dbc573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611de0919061514b565b60ff1603611e0157604051631ed7101960e21b815260040160405180910390fd5b876001600160a01b031663683fedf76040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e3f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e639190614f9d565b6001600160a01b0316639a641e0288886040518363ffffffff1660e01b8152600401611e90929190614f48565b602060405180830381865afa158015611ead573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ed1919061518a565b6001600160401b031615611ef75760405162dc1c0b60e01b815260040160405180910390fd5b608081015160405163ec5316b560e01b81526001916001600160a01b03169063ec5316b590611f2c908b908b90600401614f48565b602060405180830381865afa158015611f49573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f6d91906151a5565b1115611f8c57604051639928185760e01b815260040160405180910390fd5b60a0810151604051631a95890960e31b81526000916001600160a01b03169063d4ac484890611fc1908b908b90600401614f48565b60a060405180830381865afa158015611fde573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061200291906151be565b51905061200d612a16565b8163ffffffff16101561203b57604051634bebee7f60e01b815263ffffffff82166004820152602401610943565b508051612049908484612aea565b506000612054610f5f565b60208381015163ffffffff9081166000908152600393909301825260408084206001600160a01b038f16855280845293819020815160e081018352905460ff80821683526101008204851695830195909552600160281b810490931691810191909152600160481b82046001600160401b03166060820152600160881b820483166080820152600160901b8204831660a0820152600160981b90910465ffffffffffff1660c0820152610140850181905251919250161561213557610140820151516040516321da897960e11b815260ff9091166004820152602401610943565b612144600163ffffffff615255565b63ffffffff168261014001516040015163ffffffff16106121865760208201516040516305eee3b960e11b815263ffffffff9091166004820152602401610943565b815160ff16600090815260018201602052604090206121a5898961323a565b604084015260006121b7886040015190565b905080156121d85760405163d860955b60e01b815260040160405180910390fd5b604080850151600090815260058401602052206121f589826152bf565b50506040808401516101208501516000908152600484016020529190912061221e918d9061363e565b506040830151612232906003840190613528565b506040805160e081018252845160ff16815260006020820152610140850151820151909182019061226490600161537e565b63ffffffff1681526020018461012001516001600160401b03168152602001600060ff1681526020018760ff168152602001600065ffffffffffff168152508260000160008d6001600160a01b03166001600160a01b0316815260200190815260200160002060008201518160000160006101000a81548160ff021916908360ff16021790555060208201518160000160016101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160056101000a81548163ffffffff021916908363ffffffff16021790555060608201518160000160096101000a8154816001600160401b0302191690836001600160401b0316021790555060808201518160000160116101000a81548160ff021916908360ff16021790555060a08201518160000160126101000a81548160ff021916908360ff16021790555060c08201518160000160136101000a81548165ffffffffffff021916908365ffffffffffff1602179055509050507f2521d98b2c88e19cb124cca9f2860404ff4e038f074120bbc3d69e2c751ffc928b8461012001518b8b87602001518860000151604051612459969594939291906001600160a01b0396871681526020810195909552929094166040840152606083015263ffffffff92909216608082015260ff9190911660a082015260c00190565b60405180910390a161247b836060015184600001518486610120015188613654565b61010086015260e08501526001600160a01b031660c084018190526124c057610120830151600090815260038201602052604090206124ba908c613756565b50612527565b6040518060800160405280846020015163ffffffff1681526020018a6001600160a01b031681526020018981526020018460e001518152508361016001819052506125278360000151838d8660c0015187610120015188610100015189610160015161376b565b5050505050505050505050565b60008061253f610f5f565b63ffffffff8416600090815260039190910160209081526040808320878452600581019092529091205490915060ff1680156125a65760ff81166000908152600183016020908152604080832088845260040190915290206125a19087613c19565b610fda565b50600095945050505050565b6000806106e36125c0610f5f565b60ff851660009081526001919091016020908152604091829020825160608101845290546001600160401b0381168252600160401b810463ffffffff1692820192909252600160601b90910461ffff1691810191909152612f19565b612625826132af565b8061262e610f5f565b6001600090815260209182526040908190209290925590518281527fee3ba0763748d6e285026c82cf5f0233531ace4040dce0d5c4434c55bf5cd9399101610a00565b610b91838383612aea565b6126858361333b565b6040805160c081018252600080825260208201819052918101829052606081018290526080810182905260a0810191909152836001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa1580156126f5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127199190614f9d565b6001600160a01b0316606082015261273083610db5565b63ffffffff16602082018190526060820151612750918791906001613534565b60ff168252506000612760610f5f565b60208381015163ffffffff9081166000908152600393909301825260408084206001600160a01b038b16855280845293819020815160e081018352905460ff80821683526101008204851695830195909552600160281b810490931691810191909152600160481b82046001600160401b03166060820152600160881b820483166080820152600160901b820490921660a0830152600160981b900465ffffffffffff1660c082015290915061281581613c2e565b612837836060015184600001518484606001516001600160401b031688613654565b60a086015260808501526001600160a01b03166040840181905261286e57604051633381fb0d60e01b815260040160405180910390fd5b805160ff1660009081526001830160205260408120606083015161289c91906001600160401b03168a613c7c565b90506129058460000151848a876040015186606001516001600160401b03168960a0015160405180608001604052808c6020015163ffffffff16815260200189600001516001600160a01b03168152602001896020015181526020018c6080015181525061376b565b5050505050505050565b6129188261333b565b600061292382610db5565b9050600061292f610f5f565b63ffffffff8381166000908152600392909201602090815260408084206001600160a01b038a16855280835293819020815160e081018352905460ff80821683526101008204861694830194909452600160281b810490941691810191909152600160481b83046001600160401b03166060820152600160881b830482166080820152600160901b830490911660a0820152600160981b90910465ffffffffffff1660c08201529091506129e281613c2e565b805160ff1660009081526001808401602052604090912090612a0d9085908590849086908c90613d91565b50505050505050565b6000612a20610f5f565b60006001611105565b600080600080612a7a85612a3b610f5f565b63ffffffff808b1660009081526003929092016020908152604080842060ff8f16855260010182528084208c8552600401909152909120919061405d16565b915091508115612a8f57925060a083901c9150825b505094509492505050565b60006007612aa9836003614f8a565b6106fb9190614fd0565b60006350877ed6461480612aca5750630235ddd046145b1561142b576040518060208160008060185afa612ae357fe5b5051905090565b6040805160608101825260008082526020820181905291810191909152612b0f610f5f565b60ff8516600090815260019190910160209081526040808320815160608101835290546001600160401b0381168252600160401b810463ffffffff1693820193909352600160601b90920461ffff16908201529150612b6d84610db5565b9050816020015163ffffffff16600003612c3c5763ffffffff8116602083015281612b96610f5f565b60ff8716600081815260019290920160209081526040928390208451815486840151968601516001600160401b039092166001600160601b031990911617600160401b63ffffffff978816021761ffff60601b1916600160601b61ffff909216919091021790558251918252928416928101929092527f1122d4fb8516daedc106927e0acc4baf5f6be5f82112b501be8790859c3d4d9b910160405180910390a1612f11565b816020015163ffffffff168163ffffffff1614612f11576000612c91612c60610f5f565b60208086015163ffffffff1660009081526003929092018152604080832060ff8b168452600601909152902061302c565b90506000612cab87856020015163ffffffff168488614075565b905080600003612cc2575082516001600160401b03165b83516001600160401b03168114612deb576000612cdd610f5f565b60008381526002919091016020526040902054855160ff90911691506001600160401b031615612d3557612d0f610f5f565b85516001600160401b0316600090815260029190910160205260409020805460ff191690555b60ff811615612dbc576000612d48610f5f565b60ff8316600090815260019190910160205260408120805467ffffffffffffffff19166001600160401b039390931692909217909155612d86610f5f565b60ff8316600090815260019190910160205260409020805461ffff92909216600160601b0261ffff60601b199092169190911790555b87612dc5610f5f565b600084815260029190910160205260409020805460ff191660ff92909216919091179055505b6040518060600160405280826001600160401b031681526020018463ffffffff16815260200185600001516001600160401b031683148015612e2c57508215155b612e37576000612e55565b6020860151612e469086615255565b8660400151612e55919061539b565b61ffff169052935083612e66610f5f565b60ff8916600081815260019290920160209081526040928390208451815486840151968601516001600160401b039092166001600160601b031990911617600160401b63ffffffff978816021761ffff60601b1916600160601b61ffff9092169190910217905582519182529286169281019290925281018290527f1ffb10ddeed1be3e5ce0225fdd14d2df110d52b970118a7f9d58de2ecce3c0629060600160405180910390a150505b509392505050565b80516001600160401b031660008115612f34576103e8612f37565b60005b90508115613027576000612f49610f5f565b60026000908152602091909152604090205490506001600160a01b03811615613025576040516306d71c2560e51b8152600481018490526000906001600160a01b0383169063dae384a090602401602060405180830381865afa158015612fb4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612fd891906151a5565b9050670de0b6b3a7640000612ff582670de0b6b3a76400006142ed565b6130036103e8612710614f77565b61300d91906153b6565b6130179190614fd0565b6130219084614f8a565b9250505b505b915091565b60606000610e7b83614303565b61304161435f565b610c2457604051631afcd79f60e31b815260040160405180910390fd5b6001600160a01b0381166130a65760405162461bcd60e51b815260206004820152600f60248201526e2d32b9379031b7b73a3937b63632b960891b6044820152606401610943565b6130d58161099460017f5165972ef41194f06c5007493031d0b927c20741adcb74403b954009fd2c3618614f77565b6131044261099460017f6f55f470bdc9cb5f04223fd822021061668e4dccb43e8727b295106dc9769c8b614f77565b6131334361099460017f812a673dfca07956350df10f8a654925f561d7a0da09bdbe79e653939a14d9f1614f77565b604080516001600160a01b0383168152426020820152438183015290517f1a2dd071001ebf6e03174e3df5b305795a4ad5d41d8fdb9ba41dbbe2367134269181900360600190a150565b600080808061318c8686614379565b909450925050505b9250929050565b336001600160a01b0316816001600160a01b03166325eb1c876040518163ffffffff1660e01b8152600401602060405180830381865afa1580156131e3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906132079190614f9d565b6001600160a01b031614610849576040516302619f2b60e51b815260040160405180910390fd5b60006106f883836143bb565b60006001600160401b0382111561326757604051633995b34160e01b815260048101839052602401610943565b5067ffffffffffffffff60a01b60a09190911b166001600160a01b039091161790565b600081815260018301602052604081205415156106f8565b60606000610e7b836144b5565b604051631430d62960e21b81526001600160a01b038216906350c358a4906132db903390600401614e55565b602060405180830381865afa1580156132f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061331c9190615168565b610849573360405163451cea1760e11b81526004016109439190614e55565b806001600160a01b03166318d928316040518163ffffffff1660e01b8152600401602060405180830381865afa158015613379573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061339d9190615168565b1561084957604051635e1633d360e11b815260040160405180910390fd5b600080846001600160a01b0316633b4c9891856040518263ffffffff1660e01b81526004016133ea9190614e55565b602060405180830381865afa158015613407573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061342b91906151a5565b6040516373cf874b60e11b81529092506001600160a01b0386169063e79f0e969061345a908790600401614e55565b602060405180830381865afa158015613477573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061349b91906151a5565b9050816000036134be57604051632f6c853960e11b815260040160405180910390fd5b6134ca600060026154a9565b81161515806134f6575082600d8111156134e6576134e6614c99565b6134f19060026154a9565b811615155b610ea35782600d81111561350c5761350c614c99565b604051630d816ed160e01b815260040161094391815260200190565b60006106f883836144c0565b600080846001600160a01b0316633b4c9891876040518263ffffffff1660e01b81526004016135639190614e55565b602060405180830381865afa158015613580573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906135a491906151a5565b91508280156135b1575081155b156135cf57604051632f6c853960e11b815260040160405180910390fd5b811561360d576135dd610f5f565b63ffffffff851660009081526003919091016020908152604080832085845260050190915290205460ff16613610565b60005b9050828015613620575060ff8116155b15610d7c57604051632e8e6b7560e11b815260040160405180910390fd5b6000610830846001600160a01b0385168461450f565b600080600080886001600160a01b031663df1dd976876040518263ffffffff1660e01b815260040161368891815260200190565b600060405180830381865afa1580156136a5573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526136cd919081019061506f565b5050935050505060038160ff161061374a5760ff88166000908152600188016020908152604080832060068b0190925290912061370e908b90838a8a61452c565b92508215613748576137218184886146eb565b909550935060008490036137485760405163d92e233d60e01b815260040160405180910390fd5b505b50955095509592505050565b60006106f8836001600160a01b0384166144c0565b6001600160a01b0385811660009081526002880160209081526040808320815160a08101835289861681526001818501818152938201869052606082018690526080820186905282549081018355918552929093208251930180546001600160a01b0319811694909516938417815590519193909283916001600160a81b03191617600160a01b83600481111561380457613804614c99565b0217905550604082810151825460608086015160809687015160ff16600160e81b0260ff60e81b1963ffffffff928316600160c81b0263ffffffff60c81b1993909616600160a81b029290921667ffffffffffffffff60a81b19909416939093179390931792909216179092556001600160a01b03878116600090815260028b016020908152838220845160a0810186528c85168152600181840181815296820185905296810184905296870183905280549586018155825290208451930180546001600160a01b031981169490921693841781559151919283916001600160a81b03191617600160a01b83600481111561390157613901614c99565b021790555060408201518154606084015160809094015167ffffffffffffffff60a81b19909116600160a81b63ffffffff9384160263ffffffff60c81b191617600160c81b92909416919091029290921760ff60e81b1916600160e81b60ff90931692909202919091179055600061397761476f565b9050866002016000876001600160a01b03166001600160a01b0316815260200190815260200160002080549050876000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060000160016101000a81548163ffffffff021916908363ffffffff16021790555080876000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060000160136101000a81548165ffffffffffff021916908365ffffffffffff160217905550866002016000866001600160a01b03166001600160a01b0316815260200190815260200160002080549050876000016000876001600160a01b03166001600160a01b0316815260200190815260200160002060000160016101000a81548163ffffffff021916908363ffffffff16021790555080876000016000876001600160a01b03166001600160a01b0316815260200190815260200160002060000160136101000a81548165ffffffffffff021916908365ffffffffffff160217905550613b3a868860010160008b60ff1660ff16815260200190815260200160002060030160008781526020019081526020016000206147be90919063ffffffff16565b5060ff8816600090815260018801602090815260408083208684526003019091529020613b6790866147be565b50600080613b7c84606001519060a082901c90565b85516020808801516040808a0151815165ffffffffffff8b16815263ffffffff909516938501939093526001600160a01b03918216908401526060830191909152608082018a9052831660a082015260c0810182905260e0810188905291935091507fd7da27827582ad8f7f3f4fc8a20f7be83108736c88b28aa3b41dccf6ad6e877990610100015b60405180910390a150505050505050505050565b60006106f8836001600160a01b0384166147d3565b805160ff16600003613c53576040516315e0349160e11b815260040160405180910390fd5b602081015163ffffffff1615610849576040516366fab05760e01b815260040160405180910390fd5b60408051606080820183526000808352602083015291810191909152600083815260048501602052604081208190613cb4908561405d565b91509150600082613cc6576000613cc8565b815b90508060a081901c6020808701919091526001600160a01b039091168552600082815260058901909152604090208054613d0190615111565b80601f0160208091040260200160405190810160405280929190818152602001828054613d2d90615111565b8015613d7a5780601f10613d4f57610100808354040283529160200191613d7a565b820191906000526020600020905b815481529060010190602001808311613d5d57829003601f168201915b505050505084604001819052505050509392505050565b60608301516001600160401b031660009081526004850160205260408120819081908190613dbf908761405d565b915091508115613e0b5760608701516001600160401b031660009081526004890160205260409020613df190876147df565b50613dff60038a018261322e565b50925060a083901c9150825b60608701516001600160401b031660009081526003890160205260409020613e3390876147be565b506040518060e00160405280600060ff168152602001600063ffffffff168152602001886040015163ffffffff16815260200160006001600160401b03168152602001600060ff168152602001600060ff168152602001600065ffffffffffff16815250896000016000886001600160a01b03166001600160a01b0316815260200190815260200160002060008201518160000160006101000a81548160ff021916908360ff16021790555060208201518160000160016101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160056101000a81548163ffffffff021916908363ffffffff16021790555060608201518160000160096101000a8154816001600160401b0302191690836001600160401b0316021790555060808201518160000160116101000a81548160ff021916908360ff16021790555060a08201518160000160126101000a81548160ff021916908360ff16021790555060c08201518160000160136101000a81548165ffffffffffff021916908365ffffffffffff1602179055509050507f43123e2ba4590155d8a1fddade2bb91cc46a6ce2a49d8fba12fdeae963bc7be88688606001518c8a6000015188888b604051613c0597969594939291906001600160a01b0397881681526001600160401b0396909616602087015263ffffffff94909416604086015260ff929092166060850152909316608083015260a082019290925290151560c082015260e00190565b600080808061318c866001600160a01b038716614379565b815160009081816001600160401b0381111561409357614093614a2d565b6040519080825280602002602001820160405280156140bc578160200160208202803683370190505b50905060008060006140cc610f5f565b60008a81526003919091016020908152604080832060ff8e168452600101909152812091505b858110156141eb5760008061412c8b848151811061411257614112614ff8565b60200260200101518560000161317d90919063ffffffff16565b9150915081801561413c57508015155b156141e1578087848151811061415457614154614ff8565b602090810291909101015285158061418e57508087614174600189614f77565b8151811061418457614184614ff8565b6020026020010151105b156141a95761419e836001614f8a565b9550600194506141e1565b80876141b6600189614f77565b815181106141c6576141c6614ff8565b6020026020010151036141e157846141dd816154b5565b9550505b50506001016140f2565b5082156142e0576000826001146142185761421361420a600185614f77565b8963ffffffff16565b61421b565b60005b9050806000036142505788614231600186614f77565b8151811061424157614241614ff8565b602002602001015196506142de565b835b868110156142dc5785614266600187614f77565b8151811061427657614276614ff8565b602002602001015186828151811061429057614290614ff8565b6020026020010151036142d457816001036142c6578981815181106142b7576142b7614ff8565b602002602001015197506142dc565b816142d0816154ce565b9250505b600101614252565b505b505b5050505050949350505050565b60008183106142fc57816106f8565b5090919050565b60608160000180548060200260200160405190810160405280929190818152602001828054801561435357602002820191906000526020600020905b81548152602001906001019080831161433f575b50505050509050919050565b6000614369610f2a565b54600160401b900460ff16919050565b60008181526002830160205260408120548190806143a85761439b85856147d3565b9250600091506131949050565b600192509050613194565b509250929050565b600081815260018301602052604081205480156144a45760006143df600183614f77565b85549091506000906143f390600190614f77565b905080821461445857600086600001828154811061441357614413614ff8565b906000526020600020015490508087600001848154811061443657614436614ff8565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080614469576144696154e5565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506106fb565b60009150506106fb565b5092915050565b60606106fb8261302c565b6000818152600183016020526040812054614507575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556106fb565b5060006106fb565b600082815260028401602052604081208290556108308484613528565b600080614538866147f4565b905060018111156146e157600061455c614553600184614f77565b8563ffffffff16565b9050805b600061456c89836147fe565b604051636f8eecbb60e11b8152600481018290529091506000906001600160a01b038c169063df1dd97690602401600060405180830381865afa1580156145b7573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526145df919081019061506f565b5050935050505060038160ff16101580156146115750600082815260038a016020526040902061460e906147f4565b15155b801561461d5750818814155b80156146965750604051630562452b60e21b815260048101899052602481018390526001600160a01b038c169063158914ac90604401602060405180830381865afa158015614670573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906146949190615168565b155b156146a3575093506146de565b846146af846001614f8a565b146146c4576146bf836001614f8a565b6146c7565b60005b92508383036146d75750506146de565b5050614560565b50505b5095945050505050565b60008281526003840160205260408120819081614707826147f4565b905080156147655760008160011461473557614730614727600184614f77565b8763ffffffff16565b614738565b60005b905061474483826147fe565b600088815260048a0160205260409020909550614761908661480a565b9350505b5050935093915050565b6000614779610f5f565b6003600090815260209190915260409020546147969060016154fb565b90508065ffffffffffff166147a9610f5f565b60036000908152602091909152604090205590565b60006106f8836001600160a01b0384166143bb565b60006106f8838361328a565b60006106f8836001600160a01b03841661481f565b60006106fb825490565b60006106f8838361483c565b60006106f8836001600160a01b038416614866565b600081815260028301602052604081208190556106f8838361322e565b600082600001828154811061485357614853614ff8565b9060005260206000200154905092915050565b60008181526002830160205260408120548015801561488c575061488a84846147d3565b155b156106f85760405163015ab34360e11b815260048101849052602401610943565b6040805160a081019091526000808252602082019081526000602082018190526040820181905260609091015290565b6040805160e081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081019190915290565b6040805161018081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052610100810182905261012081019190915261014081016149776148dd565b81526040805160808101825260008082526020828101829052928201819052606082015291015290565b60ff8116811461084957600080fd5b6000602082840312156149c257600080fd5b81356106f8816149a1565b63ffffffff8116811461084957600080fd5b6001600160a01b038116811461084957600080fd5b60008060408385031215614a0757600080fd5b8235614a12816149cd565b91506020830135614a22816149df565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715614a6b57614a6b614a2d565b604052919050565b60006001600160401b03821115614a8c57614a8c614a2d565b50601f01601f191660200190565b600082601f830112614aab57600080fd5b8135614abe614ab982614a73565b614a43565b818152846020838601011115614ad357600080fd5b816020850160208301376000918101602001919091529392505050565b60008060008060808587031215614b0657600080fd5b8435614b11816149df565b93506020850135614b21816149df565b92506040850135915060608501356001600160401b03811115614b4357600080fd5b614b4f87828801614a9a565b91505092959194509250565b60008060408385031215614b6e57600080fd5b8235614b79816149a1565b91506020830135614a22816149cd565b6020808252825182820181905260009190848201906040850190845b81811015614bc157835183529284019291840191600101614ba5565b50909695505050505050565b600060208284031215614bdf57600080fd5b81356106f8816149df565b600080600060608486031215614bff57600080fd5b8335614c0a816149a1565b92506020840135614c1a816149cd565b929592945050506040919091013590565b600060208284031215614c3d57600080fd5b5035919050565b60008060408385031215614c5757600080fd5b823591506020830135614a22816149cd565b600080600060608486031215614c7e57600080fd5b8335614c89816149cd565b92506020840135614c1a816149df565b634e487b7160e01b600052602160045260246000fd5b81516001600160a01b03168152602082015160a082019060058110614ce457634e487b7160e01b600052602160045260246000fd5b80602084015250604083015163ffffffff8082166040850152806060860151166060850152505060ff608084015116608083015292915050565b600080600060608486031215614d3357600080fd5b8335614d3e816149df565b9250602084013591506040840135614d55816149cd565b809150509250925092565b6020808252825182820181905260009190848201906040850190845b81811015614bc15783516001600160a01b031683529284019291840191600101614d7c565b60008060008060808587031215614db757600080fd5b8435614dc2816149a1565b93506020850135614dd2816149cd565b92506040850135614de2816149df565b9396929550929360600135925050565b60005b83811015614e0d578181015183820152602001614df5565b50506000910152565b60008151808452614e2e816020860160208601614df2565b601f01601f19169290920160200192915050565b6020815260006106f86020830184614e16565b6001600160a01b0391909116815260200190565b60008060008060808587031215614e7f57600080fd5b8435614e8a816149df565b93506020850135925060408501356001600160401b03811115614eac57600080fd5b614eb887828801614a9a565b9250506060850135614ec9816149a1565b939692955090935050565b60008060408385031215614ee757600080fd5b8235614ef2816149df565b946020939093013593505050565b60008060008060808587031215614f1657600080fd5b8435614f21816149a1565b93506020850135614f31816149cd565b9250604085013591506060850135614ec9816149df565b6001600160a01b03929092168252602082015260400190565b634e487b7160e01b600052601160045260246000fd5b818103818111156106fb576106fb614f61565b808201808211156106fb576106fb614f61565b600060208284031215614faf57600080fd5b81516106f8816149df565b634e487b7160e01b600052601260045260246000fd5b600082614fdf57614fdf614fba565b500490565b600082614ff357614ff3614fba565b500690565b634e487b7160e01b600052603260045260246000fd5b600082601f83011261501f57600080fd5b815161502d614ab982614a73565b81815284602083860101111561504257600080fd5b610830826020830160208701614df2565b80516001600160401b038116811461506a57600080fd5b919050565b60008060008060008060c0878903121561508857600080fd5b86516001600160401b038082111561509f57600080fd5b6150ab8a838b0161500e565b975060208901519150808211156150c157600080fd5b506150ce89828a0161500e565b95505060408701516150df816149df565b60608801519094506150f0816149a1565b92506150fe60808801615053565b915060a087015190509295509295509295565b600181811c9082168061512557607f821691505b60208210810361514557634e487b7160e01b600052602260045260246000fd5b50919050565b60006020828403121561515d57600080fd5b81516106f8816149a1565b60006020828403121561517a57600080fd5b815180151581146106f857600080fd5b60006020828403121561519c57600080fd5b6106f882615053565b6000602082840312156151b757600080fd5b5051919050565b600060a082840312156151d057600080fd5b60405160a081018181106001600160401b03821117156151f2576151f2614a2d565b6040528251615200816149cd565b81526020830151615210816149cd565b60208201526040830151615223816149cd565b60408201526060830151615236816149cd565b60608201526080830151615249816149cd565b60808201529392505050565b63ffffffff8281168282160390808211156144ae576144ae614f61565b601f8211156152ba576000816000526020600020601f850160051c8101602086101561529b5750805b601f850160051c820191505b8181101561081b578281556001016152a7565b505050565b81516001600160401b038111156152d8576152d8614a2d565b6152ec816152e68454615111565b84615272565b602080601f83116001811461532157600084156153095750858301515b600019600386901b1c1916600185901b17855561081b565b600085815260208120601f198616915b8281101561535057888601518255948401946001909101908401615331565b508582101561536e5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b63ffffffff8181168382160190808211156144ae576144ae614f61565b61ffff8181168382160190808211156144ae576144ae614f61565b80820281158282048414176106fb576106fb614f61565b600181815b808511156143b35781600019048211156153ee576153ee614f61565b808516156153fb57918102915b93841c93908002906153d2565b600082615417575060016106fb565b81615424575060006106fb565b816001811461543a576002811461544457615460565b60019150506106fb565b60ff84111561545557615455614f61565b50506001821b6106fb565b5060208310610133831016604e8410600b8410161715615483575081810a6106fb565b61548d83836153cd565b80600019048211156154a1576154a1614f61565b029392505050565b60006106f88383615408565b6000600182016154c7576154c7614f61565b5060010190565b6000816154dd576154dd614f61565b506000190190565b634e487b7160e01b600052603160045260246000fd5b65ffffffffffff8181168382160190808211156144ae576144ae614f6156fe22573091f17911fb166032a3d9e0554aa73d31b7b7ddea4a4dd2995650af84bda2646970667358221220ec077ac823acdea895f5fb7d2e6b705bb9b790cb3012f4d26484cd13000f9d2e64736f6c63430008170033
Loading...
Loading
Loading...
Loading
Multichain Portfolio | 31 Chains
Chain | Token | Portfolio % | Price | Amount | Value |
---|
A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.