Contract Source Code:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
/// @custom:storage-location erc7201:openzeppelin.storage.Ownable
struct OwnableStorage {
address _owner;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;
function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
assembly ("memory-safe") {
$.slot := OwnableStorageLocation
}
}
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
function __Ownable_init(address initialOwner) internal onlyInitializing {
__Ownable_init_unchained(initialOwner);
}
function __Ownable_init_unchained(address initialOwner) internal onlyInitializing {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
OwnableStorage storage $ = _getOwnableStorage();
return $._owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
OwnableStorage storage $ = _getOwnableStorage();
address oldOwner = $._owner;
$._owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// 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 ("memory-safe") {
$.slot := INITIALIZABLE_STORAGE
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (proxy/utils/UUPSUpgradeable.sol)
pragma solidity ^0.8.20;
import {IERC1822Proxiable} from "@openzeppelin/contracts/interfaces/draft-IERC1822.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {Initializable} from "./Initializable.sol";
/**
* @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
* {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
*
* A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
* reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
* `UUPSUpgradeable` with a custom implementation of upgrades.
*
* The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
*/
abstract contract UUPSUpgradeable is Initializable, IERC1822Proxiable {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address private immutable __self = address(this);
/**
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgradeTo(address)`
* and `upgradeToAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called,
* while `upgradeToAndCall` will invoke the `receive` function if the second argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeToAndCall(address,bytes)` is present, and the second argument must
* be the empty byte string if no function should be called, making it impossible to invoke the `receive` function
* during an upgrade.
*/
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
/**
* @dev The call is from an unauthorized context.
*/
error UUPSUnauthorizedCallContext();
/**
* @dev The storage `slot` is unsupported as a UUID.
*/
error UUPSUnsupportedProxiableUUID(bytes32 slot);
/**
* @dev Check that the execution is being performed through a delegatecall call and that the execution context is
* a proxy contract with an implementation (as defined in ERC-1967) pointing to self. This should only be the case
* for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
* function through ERC-1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
* fail.
*/
modifier onlyProxy() {
_checkProxy();
_;
}
/**
* @dev Check that the execution is not being performed through a delegate call. This allows a function to be
* callable on the implementing contract but not through proxies.
*/
modifier notDelegated() {
_checkNotDelegated();
_;
}
function __UUPSUpgradeable_init() internal onlyInitializing {
}
function __UUPSUpgradeable_init_unchained() internal onlyInitializing {
}
/**
* @dev Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the
* implementation. It is used to validate the implementation's compatibility when performing an upgrade.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
*/
function proxiableUUID() external view virtual notDelegated returns (bytes32) {
return ERC1967Utils.IMPLEMENTATION_SLOT;
}
/**
* @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
* encoded in `data`.
*
* Calls {_authorizeUpgrade}.
*
* Emits an {Upgraded} event.
*
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
*/
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data);
}
/**
* @dev Reverts if the execution is not performed via delegatecall or the execution
* context is not of a proxy with an ERC-1967 compliant implementation pointing to self.
* See {_onlyProxy}.
*/
function _checkProxy() internal view virtual {
if (
address(this) == __self || // Must be called through delegatecall
ERC1967Utils.getImplementation() != __self // Must be called through an active proxy
) {
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Reverts if the execution is performed via delegatecall.
* See {notDelegated}.
*/
function _checkNotDelegated() internal view virtual {
if (address(this) != __self) {
// Must not be called through delegatecall
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upgradeToAndCall}.
*
* Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeUpgrade(address) internal onlyOwner {}
* ```
*/
function _authorizeUpgrade(address newImplementation) internal virtual;
/**
* @dev Performs an implementation upgrade with a security check for UUPS proxies, and additional setup call.
*
* As a security check, {proxiableUUID} is invoked in the new implementation, and the return value
* is expected to be the implementation slot in ERC-1967.
*
* Emits an {IERC1967-Upgraded} event.
*/
function _upgradeToAndCallUUPS(address newImplementation, bytes memory data) private {
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) {
revert UUPSUnsupportedProxiableUUID(slot);
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
} catch {
// The implementation is not UUPS
revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract ContextUpgradeable is Initializable {
function __Context_init() internal onlyInitializing {
}
function __Context_init_unchained() internal onlyInitializing {
}
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC1822.sol)
pragma solidity ^0.8.20;
/**
* @dev ERC-1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified
* proxy whose upgrades are fully controlled by the current implementation.
*/
interface IERC1822Proxiable {
/**
* @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation
* address.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy.
*/
function proxiableUUID() external view returns (bytes32);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1967.sol)
pragma solidity ^0.8.20;
/**
* @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC.
*/
interface IERC1967 {
/**
* @dev Emitted when the implementation is upgraded.
*/
event Upgraded(address indexed implementation);
/**
* @dev Emitted when the admin account has changed.
*/
event AdminChanged(address previousAdmin, address newAdmin);
/**
* @dev Emitted when the beacon is changed.
*/
event BeaconUpgraded(address indexed beacon);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/IBeacon.sol)
pragma solidity ^0.8.20;
/**
* @dev This is the interface that {BeaconProxy} expects of its beacon.
*/
interface IBeacon {
/**
* @dev Must return an address that can be used as a delegate call target.
*
* {UpgradeableBeacon} will check that this address is a contract.
*/
function implementation() external view returns (address);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (proxy/ERC1967/ERC1967Utils.sol)
pragma solidity ^0.8.21;
import {IBeacon} from "../beacon/IBeacon.sol";
import {IERC1967} from "../../interfaces/IERC1967.sol";
import {Address} from "../../utils/Address.sol";
import {StorageSlot} from "../../utils/StorageSlot.sol";
/**
* @dev This library provides getters and event emitting update functions for
* https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] slots.
*/
library ERC1967Utils {
/**
* @dev Storage slot with the address of the current implementation.
* This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1.
*/
// solhint-disable-next-line private-vars-leading-underscore
bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
/**
* @dev The `implementation` of the proxy is invalid.
*/
error ERC1967InvalidImplementation(address implementation);
/**
* @dev The `admin` of the proxy is invalid.
*/
error ERC1967InvalidAdmin(address admin);
/**
* @dev The `beacon` of the proxy is invalid.
*/
error ERC1967InvalidBeacon(address beacon);
/**
* @dev An upgrade function sees `msg.value > 0` that may be lost.
*/
error ERC1967NonPayable();
/**
* @dev Returns the current implementation address.
*/
function getImplementation() internal view returns (address) {
return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
}
/**
* @dev Stores a new address in the ERC-1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(newImplementation);
}
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
}
/**
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit IERC1967.Upgraded(newImplementation);
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}
/**
* @dev Storage slot with the admin of the contract.
* This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1.
*/
// solhint-disable-next-line private-vars-leading-underscore
bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
/**
* @dev Returns the current admin.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using
* the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
function getAdmin() internal view returns (address) {
return StorageSlot.getAddressSlot(ADMIN_SLOT).value;
}
/**
* @dev Stores a new address in the ERC-1967 admin slot.
*/
function _setAdmin(address newAdmin) private {
if (newAdmin == address(0)) {
revert ERC1967InvalidAdmin(address(0));
}
StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin;
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {IERC1967-AdminChanged} event.
*/
function changeAdmin(address newAdmin) internal {
emit IERC1967.AdminChanged(getAdmin(), newAdmin);
_setAdmin(newAdmin);
}
/**
* @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
* This is the keccak-256 hash of "eip1967.proxy.beacon" subtracted by 1.
*/
// solhint-disable-next-line private-vars-leading-underscore
bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
/**
* @dev Returns the current beacon.
*/
function getBeacon() internal view returns (address) {
return StorageSlot.getAddressSlot(BEACON_SLOT).value;
}
/**
* @dev Stores a new beacon in the ERC-1967 beacon slot.
*/
function _setBeacon(address newBeacon) private {
if (newBeacon.code.length == 0) {
revert ERC1967InvalidBeacon(newBeacon);
}
StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon;
address beaconImplementation = IBeacon(newBeacon).implementation();
if (beaconImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(beaconImplementation);
}
}
/**
* @dev Change the beacon and trigger a setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-BeaconUpgraded} event.
*
* CAUTION: Invoking this function has no effect on an instance of {BeaconProxy} since v5, since
* it uses an immutable beacon without looking at the value of the ERC-1967 beacon slot for
* efficiency.
*/
function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal {
_setBeacon(newBeacon);
emit IERC1967.BeaconUpgraded(newBeacon);
if (data.length > 0) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
} else {
_checkNonPayable();
}
}
/**
* @dev Reverts if `msg.value` is not zero. It can be used to avoid `msg.value` stuck in the contract
* if an upgrade doesn't perform an initialization call.
*/
function _checkNonPayable() private {
if (msg.value > 0) {
revert ERC1967NonPayable();
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/IERC1155.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC-1155 compliant contract, as defined in the
* https://eips.ethereum.org/EIPS/eip-1155[ERC].
*/
interface IERC1155 is IERC165 {
/**
* @dev Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`.
*/
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
/**
* @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
* transfers.
*/
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
/**
* @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
* `approved`.
*/
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
/**
* @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
*
* If an {URI} event was emitted for `id`, the standard
* https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
* returned by {IERC1155MetadataURI-uri}.
*/
event URI(string value, uint256 indexed id);
/**
* @dev Returns the value of tokens of token type `id` owned by `account`.
*/
function balanceOf(address account, uint256 id) external view returns (uint256);
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
*
* Requirements:
*
* - `accounts` and `ids` must have the same length.
*/
function balanceOfBatch(
address[] calldata accounts,
uint256[] calldata ids
) external view returns (uint256[] memory);
/**
* @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
*
* Emits an {ApprovalForAll} event.
*
* Requirements:
*
* - `operator` cannot be the zero address.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
*
* See {setApprovalForAll}.
*/
function isApprovedForAll(address account, address operator) external view returns (bool);
/**
* @dev Transfers a `value` amount of tokens of type `id` from `from` to `to`.
*
* WARNING: This function can potentially allow a reentrancy attack when transferring tokens
* to an untrusted contract, when invoking {onERC1155Received} on the receiver.
* Ensure to follow the checks-effects-interactions pattern and consider employing
* reentrancy guards when interacting with untrusted contracts.
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}.
* - `from` must have a balance of tokens of type `id` of at least `value` amount.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
* acceptance magic value.
*/
function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes calldata data) external;
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
*
* WARNING: This function can potentially allow a reentrancy attack when transferring tokens
* to an untrusted contract, when invoking {onERC1155BatchReceived} on the receiver.
* Ensure to follow the checks-effects-interactions pattern and consider employing
* reentrancy guards when interacting with untrusted contracts.
*
* Emits either a {TransferSingle} or a {TransferBatch} event, depending on the length of the array arguments.
*
* Requirements:
*
* - `ids` and `values` must have the same length.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
* acceptance magic value.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @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);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) 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 a `value` amount of tokens 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 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC-721 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`.
*
* 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;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC-721 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 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: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC-721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* 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 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 address zero.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @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);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Address.sol)
pragma solidity ^0.8.20;
import {Errors} from "./Errors.sol";
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev There's no code at `target` (it is not a contract).
*/
error AddressEmptyCode(address target);
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
if (address(this).balance < amount) {
revert Errors.InsufficientBalance(address(this).balance, amount);
}
(bool success, ) = recipient.call{value: amount}("");
if (!success) {
revert Errors.FailedCall();
}
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason or custom error, it is bubbled
* up by this function (like regular Solidity function calls). However, if
* the call reverted with no returned reason, this function reverts with a
* {Errors.FailedCall} error.
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
* was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case
* of an unsuccessful call.
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata
) internal view returns (bytes memory) {
if (!success) {
_revert(returndata);
} else {
// only check if target is a contract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
if (returndata.length == 0 && target.code.length == 0) {
revert AddressEmptyCode(target);
}
return returndata;
}
}
/**
* @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the
* revert reason or with a default {Errors.FailedCall} error.
*/
function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
if (!success) {
_revert(returndata);
} else {
return returndata;
}
}
/**
* @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedCall}.
*/
function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
assembly ("memory-safe") {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert Errors.FailedCall();
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)
pragma solidity ^0.8.20;
/**
* @dev Collection of common custom errors used in multiple contracts
*
* IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
* It is recommended to avoid relying on the error API for critical functionality.
*
* _Available since v5.1._
*/
library Errors {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error InsufficientBalance(uint256 balance, uint256 needed);
/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedCall();
/**
* @dev The deployment failed.
*/
error FailedDeployment();
/**
* @dev A necessary precompile is missing.
*/
error MissingPrecompile(address);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[ERC].
*
* 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[ERC 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
// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)
// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.
pragma solidity ^0.8.20;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
*
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
* This library helps with reading and writing to such slots without the need for inline assembly.
*
* The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
*
* Example usage to set ERC-1967 implementation slot:
* ```solidity
* contract ERC1967 {
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
*
* function _getImplementation() internal view returns (address) {
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
* }
*
* function _setImplementation(address newImplementation) internal {
* require(newImplementation.code.length > 0);
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
* }
* }
* ```
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
library StorageSlot {
struct AddressSlot {
address value;
}
struct BooleanSlot {
bool value;
}
struct Bytes32Slot {
bytes32 value;
}
struct Uint256Slot {
uint256 value;
}
struct Int256Slot {
int256 value;
}
struct StringSlot {
string value;
}
struct BytesSlot {
bytes value;
}
/**
* @dev Returns an `AddressSlot` with member `value` located at `slot`.
*/
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns a `BooleanSlot` with member `value` located at `slot`.
*/
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns a `Bytes32Slot` with member `value` located at `slot`.
*/
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns a `Uint256Slot` with member `value` located at `slot`.
*/
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns a `Int256Slot` with member `value` located at `slot`.
*/
function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns a `StringSlot` with member `value` located at `slot`.
*/
function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns an `StringSlot` representation of the string storage pointer `store`.
*/
function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
assembly ("memory-safe") {
r.slot := store.slot
}
}
/**
* @dev Returns a `BytesSlot` with member `value` located at `slot`.
*/
function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
/**
* @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
*/
function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
assembly ("memory-safe") {
r.slot := store.slot
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/BitMaps.sol)
pragma solidity ^0.8.20;
/**
* @dev Library for managing uint256 to bool mapping in a compact and efficient way, provided the keys are sequential.
* Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor].
*
* BitMaps pack 256 booleans across each bit of a single 256-bit slot of `uint256` type.
* Hence booleans corresponding to 256 _sequential_ indices would only consume a single slot,
* unlike the regular `bool` which would consume an entire slot for a single value.
*
* This results in gas savings in two ways:
*
* - Setting a zero value to non-zero only once every 256 times
* - Accessing the same warm slot for every 256 _sequential_ indices
*/
library BitMaps {
struct BitMap {
mapping(uint256 bucket => uint256) _data;
}
/**
* @dev Returns whether the bit at `index` is set.
*/
function get(BitMap storage bitmap, uint256 index) internal view returns (bool) {
uint256 bucket = index >> 8;
uint256 mask = 1 << (index & 0xff);
return bitmap._data[bucket] & mask != 0;
}
/**
* @dev Sets the bit at `index` to the boolean `value`.
*/
function setTo(BitMap storage bitmap, uint256 index, bool value) internal {
if (value) {
set(bitmap, index);
} else {
unset(bitmap, index);
}
}
/**
* @dev Sets the bit at `index`.
*/
function set(BitMap storage bitmap, uint256 index) internal {
uint256 bucket = index >> 8;
uint256 mask = 1 << (index & 0xff);
bitmap._data[bucket] |= mask;
}
/**
* @dev Unsets the bit at `index`.
*/
function unset(BitMap storage bitmap, uint256 index) internal {
uint256 bucket = index >> 8;
uint256 mask = 1 << (index & 0xff);
bitmap._data[bucket] &= ~mask;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {IBrushToken} from "../interfaces/external/IBrushToken.sol";
import {IPlayers} from "../interfaces/IPlayers.sol";
import {IClans} from "../interfaces/IClans.sol";
import {IBankFactory} from "../interfaces/IBankFactory.sol";
import {IMarketplaceWhitelist} from "../interfaces/external/IMarketplaceWhitelist.sol";
import {IClanMemberLeftCB} from "../interfaces/IClanMemberLeftCB.sol";
import {EstforLibrary} from "../EstforLibrary.sol";
import {BloomFilter} from "../libraries/BloomFilter.sol";
import {ClanRank} from "../globals/clans.sol";
contract Clans is UUPSUpgradeable, OwnableUpgradeable, IClans {
using BloomFilter for BloomFilter.Filter;
event ClanCreated(
uint256 clanId,
uint256 playerId,
string[] clanInfo,
uint256 imageId,
uint256 tierId,
uint256 createdTimestamp
);
event SetClanRank(uint256 clanId, uint256 playerId, ClanRank clan);
event InviteSent(uint256 clanId, uint256 playerId, uint256 fromPlayerId);
event InvitesSent(uint256 clanId, uint256[] playerIds, uint256 fromPlayerId);
event InviteAccepted(uint256 clanId, uint256 playerId);
event MemberLeft(uint256 clanId, uint256 playerId, uint256 removedByPlayerId);
event JoinRequestSent(uint256 clanId, uint256 playerId);
event JoinRequestAccepted(uint256 clanId, uint256 playerId, uint256 acceptedByPlayerId);
event JoinRequestsAccepted(uint256 clanId, uint256[] playerIds, uint256 acceptedByPlayerId);
event JoinRequestRemoved(uint256 clanId, uint256 playerId);
event ClanOwnershipTransferred(uint256 clanId, uint256 playerId);
event AddTiers(Tier[] tiers);
event EditTiers(Tier[] tiers);
event ClanOwnerLeft(uint256 clanId, uint256 playerId);
event ClanEdited(uint256 clanId, uint256 playerId, string[] clanInfo, uint256 imageId);
event ClanUpgraded(uint256 clanId, uint256 playerId, uint256 tierId);
event ClanDestroyed(uint256 clanId);
event PlayerRankUpdated(uint256 clanId, uint256 memberId, ClanRank rank, uint256 playerId);
event InvitesDeletedByPlayer(uint256[] clanIds, uint256 playerId);
event InvitesDeletedByClan(uint256 clanId, uint256[] invitedPlayerIds, uint256 deletedInvitesPlayerId);
event JoinRequestsRemovedByClan(uint256 clanId, uint256[] joinRequestPlayerIds, uint256 removingJoinRequestsPlayerId);
event EditNameCost(uint256 newCost);
event JoinRequestsEnabled(uint256 clanId, bool joinRequestsEnabled, uint256 playerId);
event GateKeepNFTs(uint256 clanId, address[] nfts, uint256 playerId);
event PinMessage(uint256 clanId, string message, uint256 playerId);
event SetInitialMMR(uint256 mmr);
event SetBrushDistributionPercentages(
uint256 brushBurntPercentage,
uint256 brushTreasuryPercentage,
uint256 brushDevPercentage
);
event AddXP(uint256 clanId, uint256 xp, bool xpEmittedElsewhere);
event SetMMR(uint256 clanId, uint256 mmr); // Only used by bridge currently
error AlreadyInClan();
error NotOwnerOfPlayer();
error NotOwnerOfPlayerAndActive();
error NotMemberOfClan();
error ClanIsFull();
error OwnerExists();
error InvalidImageId();
error NameTooShort();
error NameTooLong();
error NameInvalidCharacters();
error DiscordTooLong();
error DiscordTooShort();
error DiscordInvalidCharacters();
error TelegramTooLong();
error TelegramInvalidCharacters();
error TwitterTooLong();
error TwitterInvalidCharacters();
error ClanDoesNotExist();
error TierDoesNotExist();
error CannotDowngradeTier();
error TierAlreadyExists();
error NameAlreadyExists();
error ClanDestroyFailedHasMembers();
error PriceTooLow();
error MemberCapacityTooLow();
error BankCapacityTooLow();
error ImageIdTooLow();
error AlreadySentInvite();
error AlreadySentJoinRequest();
error NoJoinRequest();
error RankMustBeLowerRenounce();
error RankNotHighEnough();
error CannotSetSameRank();
error ChangingRankEqualOrHigherThanSelf();
error ChangingRankOfPlayerHigherThanSelf();
error ChangingRankOfPlayerEqualOrHigherThanSelf();
error CannotRenounceToSelf();
error InviteDoesNotExist();
error NoInvitesToDelete();
error NoJoinRequestsToDelete();
error JoinRequestsDisabled();
error TooManyNFTs();
error InvalidNFTType();
error NoGateKeptNFTFound();
error NFTNotWhitelistedOnMarketplace();
error UnsupportedNFTType();
error MessageTooLong();
error NotMMRSetter();
error PercentNotTotal100();
error PlayersAlreadySet();
error BankFactoryAlreadySet();
error ClanNameIsReserved(string name);
error NotXPModifier();
error NotBridge();
struct Clan {
uint64 ownerPlayerId;
uint16 imageId;
uint16 memberCount;
uint40 createdTimestamp;
uint8 tierId;
bool disableJoinRequests;
uint16 mmr;
uint40 xp;
string name;
mapping(uint256 playerId => bool invited) inviteRequests;
NFTInfo[] gateKeptNFTs;
}
struct PlayerInfo {
uint40 clanId; // What clan they are in
ClanRank rank; // Current clan rank
uint40 requestedClanId; // What clan they have requested to join
}
struct Tier {
uint8 id;
uint16 maxMemberCapacity;
uint16 maxBankCapacity;
uint24 maxImageId;
uint40 minimumAge; // How old the clan must be before it can be upgraded to this tier
uint80 price;
}
struct NFTInfo {
address nft;
uint80 nftType; // e.g erc721 or erc1155
}
IBrushToken private _brush;
IPlayers private _players;
IBankFactory private _bankFactory;
IERC1155 private _playerNFT;
uint40 private _nextClanId;
uint16 private _initialMMR;
address private _treasury;
uint80 private _editNameCost;
address private _dev;
uint8 private _brushBurntPercentage;
uint8 private _brushTreasuryPercentage;
uint8 private _brushDevPercentage;
address private _paintswapMarketplaceWhitelist;
IClanMemberLeftCB private _territories;
IClanMemberLeftCB private _lockedBankVaults;
IClanMemberLeftCB private _raids;
mapping(uint256 clanId => Clan clan) private _clans;
mapping(uint256 playerId => PlayerInfo) private _playerInfo;
mapping(uint256 id => Tier tier) private _tiers;
mapping(string name => bool exists) private _lowercaseNames;
mapping(uint256 clanId => uint40 timestampLeft) private _ownerlessClanTimestamps; // timestamp
mapping(address account => bool isModifier) private _xpModifiers;
BloomFilter.Filter private _reservedClanNames; // TODO: remove 90 days after launch
address private _bridge; // TODO: Bridge Can remove later if no longer need the bridge
modifier isOwnerOfPlayer(uint256 playerId) {
require(_playerNFT.balanceOf(_msgSender(), playerId) != 0, NotOwnerOfPlayer());
_;
}
modifier isOwnerOfPlayerAndActive(uint256 playerId) {
require(_players.isOwnerOfPlayerAndActive(_msgSender(), playerId), NotOwnerOfPlayerAndActive());
_;
}
modifier isMinimumRank(uint256 clanId, uint256 playerId, ClanRank rank) {
PlayerInfo storage player = _playerInfo[playerId];
require(player.clanId == clanId, NotMemberOfClan());
require(_playerInfo[playerId].rank >= rank, RankNotHighEnough());
_;
}
modifier isMemberOfClan(uint256 clanId, uint256 playerId) {
require(_playerInfo[playerId].clanId == clanId, NotMemberOfClan());
_;
}
modifier isXPModifier() {
require(_xpModifiers[_msgSender()], NotXPModifier());
_;
}
modifier onlyMMRSetter() {
require(_msgSender() == address(_lockedBankVaults), NotMMRSetter());
_;
}
modifier onlyBridge() {
require(_msgSender() == _bridge, NotBridge());
_;
}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
IBrushToken brush,
IERC1155 playerNFT,
address pool,
address dev,
uint80 editNameCost,
address paintswapMarketplaceWhitelist,
uint16 initialMMR,
uint40 startClanId,
address bridge
) external initializer {
__Ownable_init(_msgSender());
__UUPSUpgradeable_init();
_brush = brush;
_playerNFT = playerNFT;
_treasury = pool;
_dev = dev;
_nextClanId = startClanId;
_paintswapMarketplaceWhitelist = paintswapMarketplaceWhitelist;
setEditNameCost(editNameCost);
setInitialMMR(initialMMR);
_reservedClanNames._initialize(4, 420000);
_bridge = bridge;
}
function createClan(
uint256 playerId,
string calldata name,
string calldata discord,
string calldata telegram,
string calldata twitter,
uint16 imageId,
uint8 tierId
) external isOwnerOfPlayerAndActive(playerId) {
require(!isMemberOfAnyClan(playerId), AlreadyInClan());
Tier storage tier = _tiers[tierId];
_checkTierExists(tierId);
_checkClanImage(imageId, tier.maxImageId);
uint256 clanId = _nextClanId++;
Clan storage clan = _clans[clanId];
clan.ownerPlayerId = uint64(playerId);
clan.tierId = tierId;
clan.imageId = imageId;
clan.memberCount = 1;
clan.createdTimestamp = uint40(block.timestamp);
clan.mmr = _initialMMR;
PlayerInfo storage player = _playerInfo[playerId];
player.clanId = uint32(clanId);
player.rank = ClanRank.OWNER;
if (player.requestedClanId != 0) {
removeJoinRequest(player.requestedClanId, playerId);
}
(string memory trimmedName, ) = _setName(clanId, name);
_checkSocials(discord, telegram, twitter);
string[] memory clanInfo = _createClanInfo(trimmedName, discord, telegram, twitter);
emit ClanCreated(clanId, playerId, clanInfo, imageId, tierId, block.timestamp);
_pay(tier.price);
_bankFactory.createBank(_msgSender(), clanId);
}
function createClanBridge(
address from,
uint256 playerId,
uint256 clanId,
string calldata name,
string calldata discord,
string calldata telegram,
string calldata twitter,
uint256 imageId,
uint256 createdTimestamp,
uint256 tierId,
uint256 mmr,
bool disableJoinRequests
) external onlyBridge {
Clan storage clan = _clans[clanId];
clan.ownerPlayerId = uint64(playerId);
clan.tierId = uint8(tierId);
clan.imageId = uint16(imageId);
clan.memberCount = 1;
clan.createdTimestamp = uint40(createdTimestamp);
clan.mmr = uint16(mmr);
PlayerInfo storage player = _playerInfo[playerId];
player.clanId = uint32(clanId);
player.rank = ClanRank.OWNER;
// don't call _setName to avoid name reservation check
clan.name = name;
_lowercaseNames[EstforLibrary.toLower(name)] = true; // already trimmed
string[] memory clanInfo = _createClanInfo(name, discord, telegram, twitter);
emit ClanCreated(clanId, playerId, clanInfo, imageId, tierId, createdTimestamp);
if (disableJoinRequests) {
clan.disableJoinRequests = disableJoinRequests;
emit JoinRequestsEnabled(clanId, !disableJoinRequests, playerId);
}
emit SetMMR(clanId, mmr);
_bankFactory.createBank(from, clanId);
}
function editClan(
uint256 clanId,
string calldata name,
string calldata discord,
string calldata telegram,
string calldata twitter,
uint256 imageId,
uint256 playerId
) external isOwnerOfPlayerAndActive(playerId) isMinimumRank(clanId, playerId, ClanRank.LEADER) {
Clan storage clan = _clans[clanId];
Tier storage tier = _tiers[clan.tierId];
_checkClanImage(imageId, tier.maxImageId);
(string memory trimmedName, bool nameChanged) = _setName(clanId, name);
if (nameChanged) {
_pay(_editNameCost);
}
_checkSocials(discord, telegram, twitter);
string[] memory clanInfo = _createClanInfo(trimmedName, discord, telegram, twitter);
emit ClanEdited(clanId, playerId, clanInfo, imageId);
}
function deleteInvitesAsPlayer(uint256[] calldata clanIds, uint256 playerId) external isOwnerOfPlayer(playerId) {
require(clanIds.length != 0, NoInvitesToDelete());
for (uint256 i = 0; i < clanIds.length; ++i) {
uint256 clanId = clanIds[i];
require(_clans[clanId].inviteRequests[playerId], InviteDoesNotExist());
delete _clans[clanId].inviteRequests[playerId];
}
emit InvitesDeletedByPlayer(clanIds, playerId);
}
function deleteInvitesAsClan(
uint256 clanId,
uint256[] calldata invitedPlayerIds,
uint256 playerId
) external isOwnerOfPlayer(playerId) isMinimumRank(clanId, playerId, ClanRank.SCOUT) {
Clan storage clan = _clans[clanId];
require(invitedPlayerIds.length != 0, NoInvitesToDelete());
for (uint256 i = 0; i < invitedPlayerIds.length; ++i) {
uint256 invitedPlayerId = invitedPlayerIds[i];
require(clan.inviteRequests[invitedPlayerId], InviteDoesNotExist());
clan.inviteRequests[invitedPlayerId] = false;
}
emit InvitesDeletedByClan(clanId, invitedPlayerIds, playerId);
}
function inviteMembers(
uint256 clanId,
uint256[] calldata memberPlayerIds,
uint256 playerId
) external isOwnerOfPlayer(playerId) isMinimumRank(clanId, playerId, ClanRank.SCOUT) {
Clan storage clan = _clans[clanId];
Tier storage tier = _tiers[clan.tierId];
require(clan.memberCount + memberPlayerIds.length <= tier.maxMemberCapacity, ClanIsFull());
for (uint256 i = 0; i < memberPlayerIds.length; ++i) {
_inviteMember(clanId, memberPlayerIds[i]);
}
emit InvitesSent(clanId, memberPlayerIds, playerId);
}
function _acceptInvite(uint256 clanId, uint256 playerId, uint256 gateKeepTokenId) private {
Clan storage clan = _clans[clanId];
require(clan.inviteRequests[playerId], InviteDoesNotExist());
require(!isMemberOfAnyClan(playerId), AlreadyInClan());
_checkGateKeeping(clanId, gateKeepTokenId);
Tier storage tier = _tiers[clan.tierId];
require(clan.memberCount < tier.maxMemberCapacity, ClanIsFull());
clan.inviteRequests[playerId] = false;
clan.memberCount++;
PlayerInfo storage player = _playerInfo[playerId];
player.clanId = uint32(clanId);
player.rank = ClanRank.COMMONER;
player.requestedClanId = 0;
emit InviteAccepted(clanId, playerId);
}
function acceptInvite(
uint256 clanId,
uint256 playerId,
uint256 gateKeepTokenId
) external isOwnerOfPlayerAndActive(playerId) {
_acceptInvite(clanId, playerId, gateKeepTokenId);
}
function requestToJoin(
uint256 clanId,
uint256 playerId,
uint256 gateKeepTokenId
) external isOwnerOfPlayerAndActive(playerId) {
_requestToJoin(clanId, playerId, gateKeepTokenId);
}
function removeJoinRequest(uint256 clanId, uint256 playerId) public isOwnerOfPlayer(playerId) {
_playerInfo[playerId].requestedClanId = 0;
emit JoinRequestRemoved(clanId, playerId);
}
function removeJoinRequestsAsClan(
uint256 clanId,
uint256[] calldata joinRequestPlayerIds,
uint256 playerId
) external isOwnerOfPlayer(playerId) isMinimumRank(clanId, playerId, ClanRank.SCOUT) {
require(joinRequestPlayerIds.length != 0, NoJoinRequestsToDelete());
for (uint256 i = 0; i < joinRequestPlayerIds.length; ++i) {
uint256 joinRequestPlayerId = joinRequestPlayerIds[i];
PlayerInfo storage playerInfo = _playerInfo[joinRequestPlayerId];
require(playerInfo.requestedClanId == clanId, NoJoinRequest());
playerInfo.requestedClanId = 0;
}
emit JoinRequestsRemovedByClan(clanId, joinRequestPlayerIds, playerId);
}
function acceptJoinRequests(
uint256 clanId,
uint256[] calldata newMemberPlayerIds,
uint256 playerId
) public isOwnerOfPlayerAndActive(playerId) isMinimumRank(clanId, playerId, ClanRank.SCOUT) {
Clan storage clan = _clans[clanId];
Tier storage tier = _tiers[clan.tierId];
require(clan.memberCount + newMemberPlayerIds.length <= tier.maxMemberCapacity, ClanIsFull());
for (uint256 i = 0; i < newMemberPlayerIds.length; ++i) {
_acceptJoinRequest(clanId, newMemberPlayerIds[i]);
}
emit JoinRequestsAccepted(clanId, newMemberPlayerIds, playerId);
}
function changeRank(
uint256 clanId,
uint256 memberId,
ClanRank rank,
uint256 playerId
) public isOwnerOfPlayer(playerId) isMemberOfClan(clanId, memberId) {
ClanRank currentMemberRank = _playerInfo[memberId].rank;
ClanRank callerRank = _playerInfo[playerId].rank;
bool changingSelf = memberId == playerId;
require(callerRank > rank, ChangingRankEqualOrHigherThanSelf());
// Cannot change Rank of someone higher or equal yourself
if (changingSelf) {
require(callerRank >= currentMemberRank, ChangingRankOfPlayerHigherThanSelf());
} else {
require(callerRank > currentMemberRank, ChangingRankOfPlayerEqualOrHigherThanSelf());
}
require(currentMemberRank != rank, CannotSetSameRank());
bool isDemoting = currentMemberRank > rank;
if (isDemoting) {
// Are they leaving?
if (rank == ClanRank.NONE) {
_removeFromClan(clanId, memberId, playerId);
} else {
// If owner is leaving their post then we need to update the owned state
if (currentMemberRank == ClanRank.OWNER) {
_ownerCleared(clanId);
}
_updateRank(clanId, memberId, rank, playerId);
}
} else {
// Promoting
_updateRank(clanId, memberId, rank, playerId);
}
}
function changeRanks(
uint256 clanId,
uint256[] calldata memberIds,
ClanRank[] calldata ranks,
uint256 playerId
) external isOwnerOfPlayer(playerId) {
for (uint256 i = 0; i < memberIds.length; ++i) {
changeRank(clanId, memberIds[i], ranks[i], playerId);
}
}
function renounceOwnershipTo(
uint256 clanId,
uint256 newOwnerPlayerId,
ClanRank newRank
) external isOwnerOfPlayer(_clans[clanId].ownerPlayerId) isMemberOfClan(clanId, newOwnerPlayerId) {
Clan storage clan = _clans[clanId];
uint256 oldOwnerPlayerId = clan.ownerPlayerId;
require(newOwnerPlayerId != oldOwnerPlayerId, CannotRenounceToSelf());
if (newRank != ClanRank.NONE) {
require(newRank < ClanRank.OWNER, RankMustBeLowerRenounce());
// Change old owner to new rank
_updateRank(clanId, oldOwnerPlayerId, newRank, oldOwnerPlayerId);
} else {
_removeFromClan(clanId, oldOwnerPlayerId, oldOwnerPlayerId);
}
_claimOwnership(clanId, newOwnerPlayerId);
}
// Can claim a clan if there is no owner
function claimOwnership(
uint256 clanId,
uint256 playerId
) external isOwnerOfPlayer(playerId) isMemberOfClan(clanId, playerId) {
Clan storage clan = _clans[clanId];
require(clan.ownerPlayerId == 0, OwnerExists());
_claimOwnership(clanId, playerId);
}
function setJoinRequestsEnabled(
uint256 clanId,
bool joinRequestsEnabled,
uint256 playerId
) external isOwnerOfPlayer(playerId) isMinimumRank(clanId, playerId, ClanRank.SCOUT) {
Clan storage clan = _clans[clanId];
clan.disableJoinRequests = !joinRequestsEnabled;
emit JoinRequestsEnabled(clanId, joinRequestsEnabled, playerId);
}
function upgradeClan(uint256 clanId, uint256 playerId, uint8 newTierId) public isOwnerOfPlayer(playerId) {
_upgradeClan(clanId, playerId, newTierId);
}
function pinMessage(
uint256 clanId,
string calldata message,
uint256 playerId
) external isOwnerOfPlayerAndActive(playerId) isMinimumRank(clanId, playerId, ClanRank.LEADER) {
require(bytes(message).length <= 200, MessageTooLong());
emit PinMessage(clanId, message, playerId);
}
function gateKeep(
uint256 clanId,
NFTInfo[] calldata nftInfos,
uint256 playerId
) external isOwnerOfPlayerAndActive(playerId) isMinimumRank(clanId, playerId, ClanRank.LEADER) {
require(nftInfos.length <= 5, TooManyNFTs());
address[] memory nfts = new address[](nftInfos.length);
IMarketplaceWhitelist paintswapMarketplaceWhitelist = IMarketplaceWhitelist(_paintswapMarketplaceWhitelist);
for (uint256 i; i < nftInfos.length; ++i) {
// This must be whitelisted by the PaintSwapMarketplace marketplace
address nft = nftInfos[i].nft;
require(paintswapMarketplaceWhitelist.isWhitelisted(nft), NFTNotWhitelistedOnMarketplace());
// Must be a supported NFT standard
uint256 nftType = nftInfos[i].nftType;
// Checks supportsInterface is correct
if (nftType == 721) {
require(IERC721(nft).supportsInterface(type(IERC721).interfaceId), InvalidNFTType());
} else if (nftType == 1155) {
require(IERC1155(nft).supportsInterface(type(IERC1155).interfaceId), InvalidNFTType());
} else {
revert UnsupportedNFTType();
}
nfts[i] = nft;
}
_clans[clanId].gateKeptNFTs = nftInfos;
emit GateKeepNFTs(clanId, nfts, playerId);
}
// The flag is for cases where XP is added in the future and not part of those events
function addXP(uint256 clanId, uint40 xp, bool xpEmittedElsewhere) external isXPModifier {
_clans[clanId].xp += xp;
emit AddXP(clanId, xp, xpEmittedElsewhere);
}
function setMMR(uint256 clanId, uint16 mmr) external onlyMMRSetter {
_clans[clanId].mmr = mmr;
}
function _checkClanImage(uint256 imageId, uint256 maxImageId) private pure {
require(imageId != 0 && imageId <= maxImageId, InvalidImageId());
}
function _setName(
uint256 clanId,
string calldata name
) private returns (string memory trimmedName, bool nameChanged) {
// Trimmed name cannot be empty
trimmedName = EstforLibrary.trim(name);
require(bytes(trimmedName).length >= 3, NameTooShort());
require(bytes(trimmedName).length <= 20, NameTooLong());
require(EstforLibrary.containsValidNameCharacters(trimmedName), NameInvalidCharacters());
string memory trimmedAndLowercaseName = EstforLibrary.toLower(trimmedName);
Clan storage clan = _clans[clanId];
string memory oldName = EstforLibrary.toLower(clan.name);
nameChanged = keccak256(abi.encodePacked(oldName)) != keccak256(abi.encodePacked(trimmedAndLowercaseName));
if (nameChanged) {
require(
!_reservedClanNames._probablyContainsString(trimmedAndLowercaseName),
ClanNameIsReserved(trimmedAndLowercaseName)
);
require(!_lowercaseNames[trimmedAndLowercaseName], NameAlreadyExists());
if (bytes(oldName).length != 0) {
delete _lowercaseNames[oldName];
}
_lowercaseNames[trimmedAndLowercaseName] = true;
clan.name = trimmedName;
}
}
function _checkSocials(string calldata discord, string calldata telegram, string calldata twitter) private pure {
uint256 discordLength = bytes(discord).length;
require(discordLength <= 25, DiscordTooLong());
require(discordLength == 0 || discordLength >= 4, DiscordTooShort());
require(EstforLibrary.containsBaselineSocialNameCharacters(discord), DiscordInvalidCharacters());
uint256 telegramLength = bytes(telegram).length;
require(telegramLength <= 25, TelegramTooLong());
require(EstforLibrary.containsBaselineSocialNameCharacters(telegram), TelegramInvalidCharacters());
uint256 twitterLength = bytes(twitter).length;
require(twitterLength <= 25, TwitterTooLong());
require(EstforLibrary.containsBaselineSocialNameCharacters(twitter), TwitterInvalidCharacters());
}
function _createClanInfo(
string memory trimmedName,
string calldata discord,
string calldata telegram,
string calldata twitter
) private pure returns (string[] memory clanInfo) {
clanInfo = new string[](4);
clanInfo[0] = trimmedName;
clanInfo[1] = discord;
clanInfo[2] = telegram;
clanInfo[3] = twitter;
}
function _checkGateKeeping(uint256 clanId, uint256 gateKeepTokenId) private view {
NFTInfo[] memory nftInfo = _clans[clanId].gateKeptNFTs;
if (nftInfo.length != 0) {
bool foundNFT;
// Check the player owns one of these NFTs
address sender = _msgSender();
for (uint256 i = 0; i < nftInfo.length; ++i) {
if (nftInfo[i].nftType == 1155) {
foundNFT = foundNFT || IERC1155(nftInfo[i].nft).balanceOf(sender, gateKeepTokenId) != 0;
} else if (nftInfo[i].nftType == 721) {
foundNFT = foundNFT || IERC721(nftInfo[i].nft).ownerOf(gateKeepTokenId) == sender;
}
}
require(foundNFT, NoGateKeptNFTFound());
}
}
function _ownerCleared(uint256 clanId) private {
Clan storage clan = _clans[clanId];
uint256 oldOwnerPlayerId = clan.ownerPlayerId;
clan.ownerPlayerId = 0;
_ownerlessClanTimestamps[clanId] = uint40(block.timestamp);
emit ClanOwnerLeft(clanId, oldOwnerPlayerId);
}
function _updateRank(uint256 clanId, uint256 memberId, ClanRank rank, uint256 playerId) private {
PlayerInfo storage player = _playerInfo[memberId];
player.rank = rank;
emit PlayerRankUpdated(clanId, memberId, rank, playerId);
}
function _destroyClan(uint256 clanId) private {
// Defensive check
Clan storage clan = _clans[clanId];
require(clan.memberCount == 0, ClanDestroyFailedHasMembers());
_lowercaseNames[EstforLibrary.toLower(clan.name)] = false; // Name can be used again
delete _clans[clanId]; // Delete the clan
emit ClanDestroyed(clanId);
}
function _removeFromClan(uint256 clanId, uint256 playerId, uint256 removingPlayerId) private {
Clan storage clan = _clans[clanId];
if (clan.ownerPlayerId == playerId) {
_ownerCleared(clanId);
}
clan.memberCount--;
if (clan.memberCount == 0) {
_destroyClan(clanId);
} else {
emit MemberLeft(clanId, playerId, removingPlayerId);
}
PlayerInfo storage player = _playerInfo[playerId];
player.clanId = 0;
player.rank = ClanRank.NONE;
_territories.clanMemberLeft(clanId, playerId);
_lockedBankVaults.clanMemberLeft(clanId, playerId);
_raids.clanMemberLeft(clanId, playerId);
}
function _claimOwnership(uint256 clanId, uint256 playerId) private {
Clan storage clan = _clans[clanId];
clan.ownerPlayerId = uint64(playerId);
delete _ownerlessClanTimestamps[clanId];
_playerInfo[playerId].rank = ClanRank.OWNER;
emit ClanOwnershipTransferred(clanId, playerId);
}
function _pay(uint256 tokenCost) private {
IBrushToken brush = _brush;
address sender = _msgSender();
brush.burnFrom(sender, (tokenCost * _brushBurntPercentage) / 100);
brush.transferFrom(sender, _treasury, (tokenCost * _brushTreasuryPercentage) / 100);
brush.transferFrom(sender, _dev, (tokenCost * _brushDevPercentage) / 100);
}
function _upgradeClan(uint256 clanId, uint256 playerId, uint8 newTierId) private {
Tier storage oldTier = _tiers[_clans[clanId].tierId];
require(oldTier.id != 0, ClanDoesNotExist());
require(newTierId > oldTier.id, CannotDowngradeTier());
// require(_playerInfo[playerId].clanId == clanId, NotMemberOfClan());
_checkTierExists(newTierId);
Tier storage newTier = _tiers[newTierId];
uint256 priceDifference = newTier.price - oldTier.price;
_pay(priceDifference);
_clans[clanId].tierId = newTierId; // Increase the tier
emit ClanUpgraded(clanId, playerId, newTierId);
}
function _setTier(Tier calldata tier) private {
uint256 tierId = tier.id;
// TODO: Some other checks
// Price should be higher than the one prior
if (tierId > 1) {
Tier memory priorTier = _tiers[tierId - 1];
require(tier.price >= priorTier.price, PriceTooLow());
require(tier.maxMemberCapacity >= priorTier.maxMemberCapacity, MemberCapacityTooLow());
require(tier.maxBankCapacity >= priorTier.maxBankCapacity, BankCapacityTooLow());
require(tier.maxImageId >= priorTier.maxImageId, ImageIdTooLow());
}
_tiers[tierId] = tier;
}
function _checkTierExists(uint256 tierId) private view {
Tier storage tier = _tiers[tierId];
require(tier.id != 0, TierDoesNotExist());
}
function _inviteMember(uint256 clanId, uint256 member) private {
Clan storage clan = _clans[clanId];
require(!clan.inviteRequests[member], AlreadySentInvite());
clan.inviteRequests[member] = true;
}
function _requestToJoin(uint256 clanId, uint256 playerId, uint256 gateKeepTokenId) private {
Clan storage clan = _clans[clanId];
require(clan.createdTimestamp != 0, ClanDoesNotExist());
require(!clan.disableJoinRequests, JoinRequestsDisabled());
_checkGateKeeping(clanId, gateKeepTokenId);
PlayerInfo storage player = _playerInfo[playerId];
require(!isMemberOfAnyClan(playerId), AlreadyInClan());
uint256 playerRequestedClanId = player.requestedClanId;
if (playerRequestedClanId != 0) {
require(playerRequestedClanId != clanId, AlreadySentJoinRequest());
emit JoinRequestRemoved(playerRequestedClanId, playerId);
}
player.requestedClanId = uint32(clanId);
emit JoinRequestSent(clanId, playerId);
}
function _acceptJoinRequest(uint256 clanId, uint256 newMemberPlayerId) private {
Clan storage clan = _clans[clanId];
clan.inviteRequests[newMemberPlayerId] = false;
clan.memberCount++;
PlayerInfo storage player = _playerInfo[newMemberPlayerId];
require(player.requestedClanId == clanId, NoJoinRequest());
player.clanId = uint32(clanId);
player.requestedClanId = 0;
player.rank = ClanRank.COMMONER;
}
function getClanIdFromPlayer(uint256 playerId) external view returns (uint256) {
return _playerInfo[playerId].clanId;
}
function getClanNameOfPlayer(uint256 playerId) external view returns (string memory) {
uint256 clanId = _playerInfo[playerId].clanId;
return _clans[clanId].name;
}
function canWithdraw(uint256 clanId, uint256 playerId) external view override returns (bool) {
return _playerInfo[playerId].clanId == clanId && _playerInfo[playerId].rank >= ClanRank.TREASURER;
}
function isClanMember(uint256 clanId, uint256 playerId) external view returns (bool) {
return _playerInfo[playerId].clanId == clanId;
}
function isMemberOfAnyClan(uint256 playerId) public view returns (bool) {
return _playerInfo[playerId].clanId != 0;
}
function getClanTierMembership(uint256 playerId) external view returns (uint8) {
return _clans[_playerInfo[playerId].clanId].tierId;
}
function getClanId(uint256 playerId) external view returns (uint256) {
return _playerInfo[playerId].clanId;
}
function getMMR(uint256 clanId) external view returns (uint16 mmr) {
mmr = _clans[clanId].mmr;
}
function hasInviteRequest(uint256 clanId, uint256 playerId) external view returns (bool) {
return _clans[clanId].inviteRequests[playerId];
}
function maxBankCapacity(uint256 clanId) external view override returns (uint16) {
Tier storage tier = _tiers[_clans[clanId].tierId];
return tier.maxBankCapacity;
}
function maxMemberCapacity(uint256 clanId) external view override returns (uint16) {
Tier storage tier = _tiers[_clans[clanId].tierId];
return tier.maxMemberCapacity;
}
function getRank(uint256 clanId, uint256 playerId) external view returns (ClanRank rank) {
if (_playerInfo[playerId].clanId == clanId) {
return _playerInfo[playerId].rank;
}
return ClanRank.NONE;
}
function getEditNameCost() external view returns (uint80) {
return _editNameCost;
}
function getPlayerInfo(uint256 playerId) external view returns (PlayerInfo memory) {
return _playerInfo[playerId];
}
function getLowercaseNames(string calldata name) external view returns (bool) {
return _lowercaseNames[name];
}
function getTier(uint256 tierId) external view returns (Tier memory) {
return _tiers[tierId];
}
function getClan(
uint256 clanId
)
external
view
returns (
uint64 ownerPlayerId,
uint16 imageId,
uint16 memberCount,
uint40 createdTimestamp,
uint8 tierId,
bool disableJoinRequests,
uint16 mmr,
string memory name,
NFTInfo[] memory gateKeptNFTs
)
{
Clan storage clan = _clans[clanId];
return (
clan.ownerPlayerId,
clan.imageId,
clan.memberCount,
clan.createdTimestamp,
clan.tierId,
clan.disableJoinRequests,
clan.mmr,
clan.name,
clan.gateKeptNFTs
);
}
function isClanNameReserved(string calldata clanName) public view returns (bool) {
return _reservedClanNames._probablyContainsString(EstforLibrary.toLower(clanName));
}
function addTiers(Tier[] calldata tiers) external onlyOwner {
for (uint256 i; i < tiers.length; ++i) {
require(tiers[i].id != 0 && _tiers[tiers[i].id].id == 0, TierAlreadyExists());
_setTier(tiers[i]);
}
emit AddTiers(tiers);
}
function editTiers(Tier[] calldata tiers) external onlyOwner {
for (uint256 i; i < tiers.length; ++i) {
_checkTierExists(tiers[i].id);
_setTier(tiers[i]);
}
emit EditTiers(tiers);
}
function initializeAddresses(
IPlayers players,
IBankFactory bankFactory,
IClanMemberLeftCB territories,
IClanMemberLeftCB lockedBankVaults,
IClanMemberLeftCB raids
) external onlyOwner {
require(address(_bankFactory) == address(0) || _bankFactory == bankFactory, BankFactoryAlreadySet());
_players = players;
_bankFactory = bankFactory;
_territories = territories;
_lockedBankVaults = lockedBankVaults;
_raids = raids;
}
function setXPModifiers(address[] calldata accounts, bool isModifier) external onlyOwner {
for (uint256 i; i < accounts.length; ++i) {
_xpModifiers[accounts[i]] = isModifier;
}
}
function setEditNameCost(uint80 editNameCost) public onlyOwner {
_editNameCost = editNameCost;
emit EditNameCost(editNameCost);
}
function setInitialMMR(uint16 mmr) public onlyOwner {
_initialMMR = mmr;
emit SetInitialMMR(mmr);
}
function setBrushDistributionPercentages(
uint8 brushBurntPercentage,
uint8 brushTreasuryPercentage,
uint8 brushDevPercentage
) external onlyOwner {
require(brushBurntPercentage + brushTreasuryPercentage + brushDevPercentage == 100, PercentNotTotal100());
_brushBurntPercentage = brushBurntPercentage;
_brushTreasuryPercentage = brushTreasuryPercentage;
_brushDevPercentage = brushDevPercentage;
emit SetBrushDistributionPercentages(brushBurntPercentage, brushTreasuryPercentage, brushDevPercentage);
}
function setReservedNameBits(uint256[] calldata positions) external onlyOwner {
_reservedClanNames._addPositions(positions);
}
function addReservedClanNames(string[] calldata names) external onlyOwner {
for (uint256 i = 0; i < names.length; ++i) {
_reservedClanNames._addString(EstforLibrary.toLower(names[i]));
}
}
// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IPlayers} from "./interfaces/IPlayers.sol";
// solhint-disable-next-line no-global-import
import "./globals/all.sol";
// This file contains methods for interacting with generic functions like trimming strings, lowercase etc.
// Also has some shared functions for rewards
library EstforLibrary {
error RandomRewardsMustBeInOrder(uint16 chance1, uint16 chance2);
error RandomRewardNoDuplicates();
error GuaranteedRewardsNoDuplicates();
error TooManyGuaranteedRewards();
error TooManyRandomRewards();
function isWhitespace(bytes1 _char) internal pure returns (bool) {
return
_char == 0x20 || // Space
_char == 0x09 || // Tab
_char == 0x0a || // Line feed
_char == 0x0D || // Carriage return
_char == 0x0B || // Vertical tab
_char == 0x00; // empty byte
}
function leftTrim(string memory str) internal pure returns (string memory) {
bytes memory b = bytes(str);
uint256 strLen = b.length;
uint256 start = type(uint256).max;
// Find the index of the first non-whitespace character
for (uint256 i = 0; i < strLen; ++i) {
bytes1 char = b[i];
if (!isWhitespace(char)) {
start = i;
break;
}
}
if (start == type(uint256).max) {
return "";
}
// Copy the remainder to a new string
bytes memory trimmedBytes = new bytes(strLen - start);
for (uint256 i = start; i < strLen; ++i) {
trimmedBytes[i - start] = b[i];
}
return string(trimmedBytes);
}
function rightTrim(string calldata str) internal pure returns (string memory) {
bytes memory b = bytes(str);
uint256 strLen = b.length;
if (strLen == 0) {
return "";
}
int end = -1;
// Find the index of the last non-whitespace character
for (int i = int(strLen) - 1; i >= 0; --i) {
bytes1 char = b[uint256(i)];
if (!isWhitespace(char)) {
end = i;
break;
}
}
if (end == -1) {
return "";
}
bytes memory trimmedBytes = new bytes(uint256(end) + 1);
for (uint256 i = 0; i <= uint256(end); ++i) {
trimmedBytes[i] = b[i];
}
return string(trimmedBytes);
}
function trim(string calldata str) external pure returns (string memory) {
return leftTrim(rightTrim(str));
}
// Assumes the string is already trimmed
function containsValidNameCharacters(string calldata name) external pure returns (bool) {
bytes memory b = bytes(name);
bool lastCharIsWhitespace;
for (uint256 i = 0; i < b.length; ++i) {
bytes1 char = b[i];
bool isUpperCaseLetter = (char >= 0x41) && (char <= 0x5A); // A-Z
bool isLowerCaseLetter = (char >= 0x61) && (char <= 0x7A); // a-z
bool isDigit = (char >= 0x30) && (char <= 0x39); // 0-9
bool isSpecialCharacter = (char == 0x2D) || (char == 0x5F) || (char == 0x2E) || (char == 0x20); // "-", "_", ".", and " "
bool _isWhitespace = isWhitespace(char);
bool hasMultipleWhitespaceInRow = lastCharIsWhitespace && _isWhitespace;
lastCharIsWhitespace = _isWhitespace;
if ((!isUpperCaseLetter && !isLowerCaseLetter && !isDigit && !isSpecialCharacter) || hasMultipleWhitespaceInRow) {
return false;
}
}
return true;
}
function containsValidDiscordCharacters(string calldata discord) external pure returns (bool) {
bytes memory discordBytes = bytes(discord);
for (uint256 i = 0; i < discordBytes.length; ++i) {
bytes1 char = discordBytes[i];
bool isUpperCaseLetter = (char >= 0x41) && (char <= 0x5A); // A-Z
bool isLowerCaseLetter = (char >= 0x61) && (char <= 0x7A); // a-z
bool isDigit = (char >= 0x30) && (char <= 0x39); // 0-9
if (!isUpperCaseLetter && !isLowerCaseLetter && !isDigit) {
return false;
}
}
return true;
}
function containsValidTelegramCharacters(string calldata telegram) external pure returns (bool) {
bytes memory telegramBytes = bytes(telegram);
for (uint256 i = 0; i < telegramBytes.length; ++i) {
bytes1 char = telegramBytes[i];
bool isUpperCaseLetter = (char >= 0x41) && (char <= 0x5A); // A-Z
bool isLowerCaseLetter = (char >= 0x61) && (char <= 0x7A); // a-z
bool isDigit = (char >= 0x30) && (char <= 0x39); // 0-9
bool isPlus = char == 0x2B; // "+"
if (!isUpperCaseLetter && !isLowerCaseLetter && !isDigit && !isPlus) {
return false;
}
}
return true;
}
function containsValidTwitterCharacters(string calldata twitter) external pure returns (bool) {
bytes memory twitterBytes = bytes(twitter);
for (uint256 i = 0; i < twitterBytes.length; ++i) {
bytes1 char = twitterBytes[i];
bool isUpperCaseLetter = (char >= 0x41) && (char <= 0x5A); // A-Z
bool isLowerCaseLetter = (char >= 0x61) && (char <= 0x7A); // a-z
bool isDigit = (char >= 0x30) && (char <= 0x39); // 0-9
if (!isUpperCaseLetter && !isLowerCaseLetter && !isDigit) {
return false;
}
}
return true;
}
function containsBaselineSocialNameCharacters(string calldata socialMediaName) external pure returns (bool) {
bytes memory socialMediaNameBytes = bytes(socialMediaName);
for (uint256 i = 0; i < socialMediaNameBytes.length; ++i) {
bytes1 char = socialMediaNameBytes[i];
bool isUpperCaseLetter = (char >= 0x41) && (char <= 0x5A); // A-Z
bool isLowerCaseLetter = (char >= 0x61) && (char <= 0x7A); // a-z
bool isDigit = (char >= 0x30) && (char <= 0x39); // 0-9
bool isUnderscore = char == 0x5F; // "_"
bool isPeriod = char == 0x2E; // "."
bool isPlus = char == 0x2B; // "+"
if (!isUpperCaseLetter && !isLowerCaseLetter && !isDigit && !isUnderscore && !isPeriod && !isPlus) {
return false;
}
}
return true;
}
function toLower(string memory str) internal pure returns (string memory) {
bytes memory lowerStr = abi.encodePacked(str);
for (uint256 i = 0; i < lowerStr.length; ++i) {
bytes1 char = lowerStr[i];
if ((char >= 0x41) && (char <= 0x5A)) {
// So we add 32 to make it lowercase
lowerStr[i] = bytes1(uint8(char) + 32);
}
}
return string(lowerStr);
}
// This should match the one below, useful when a calldata array is needed and for external testing
function _binarySearchMemory(uint64[] calldata array, uint256 target) internal pure returns (uint256) {
uint256 low = 0;
uint256 high = array.length - 1;
while (low <= high) {
uint256 mid = low + (high - low) / 2;
if (array[mid] == target) {
return mid; // Element found
} else if (array[mid] < target) {
low = mid + 1;
} else {
// Check to prevent underflow
if (mid != 0) {
high = mid - 1;
} else {
// If mid is 0 and _arr[mid] is not the target, the element is not in the array
break;
}
}
}
return type(uint256).max; // Element not found
}
function binarySearchMemory(uint64[] calldata array, uint256 target) external pure returns (uint256) {
return _binarySearchMemory(array, target);
}
// This should match the one above
function _binarySearch(uint64[] storage array, uint256 target) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length - 1;
while (low <= high) {
uint256 mid = low + (high - low) / 2;
if (array[mid] == target) {
return mid; // Element found
} else if (array[mid] < target) {
low = mid + 1;
} else {
// Check to prevent underflow
if (mid != 0) {
high = mid - 1;
} else {
// If mid is 0 and _arr[mid] is not the target, the element is not in the array
break;
}
}
}
return type(uint256).max; // Element not found
}
function binarySearch(uint64[] storage array, uint256 target) external view returns (uint256) {
return _binarySearch(array, target);
}
function _shuffleArray(uint64[] memory array, uint256 randomNumber) internal pure returns (uint64[] memory output) {
for (uint256 i; i < array.length; ++i) {
uint256 n = i + (randomNumber % (array.length - i));
if (i != n) {
uint64 temp = array[n];
array[n] = array[i];
array[i] = temp;
}
}
return array;
}
function _getRandomInRange16(
uint256 randomWord,
uint256 shift,
int16 minValue,
int16 maxValue
) internal pure returns (int16) {
return int16(minValue + (int16(int256((randomWord >> shift) & 0xFFFF) % (maxValue - minValue + 1))));
}
function _getRandomFromArray16(
uint256 randomWord,
uint256 shift,
uint16[] storage arr,
uint256 arrLength
) internal view returns (uint16) {
return arr[_getRandomIndexFromArray16(randomWord, shift, arrLength)];
}
function _getRandomFrom3ElementArray16(
uint256 randomWord,
uint256 shift,
uint16[3] memory arr
) internal pure returns (uint16) {
return arr[_getRandomIndexFromArray16(randomWord, shift, arr.length)];
}
function _getRandomIndexFromArray16(
uint256 randomWord,
uint256 shift,
uint256 arrLength
) internal pure returns (uint16) {
return uint16(((randomWord >> shift) & 0xFFFF) % arrLength);
}
function setActionGuaranteedRewards(
GuaranteedReward[] calldata guaranteedRewards,
ActionRewards storage actionRewards
) external {
_setActionGuaranteedRewards(guaranteedRewards, actionRewards);
}
function setActionRandomRewards(RandomReward[] calldata randomRewards, ActionRewards storage actionRewards) external {
_setActionRandomRewards(randomRewards, actionRewards);
}
function _setActionGuaranteedRewards(
GuaranteedReward[] calldata guaranteedRewards,
ActionRewards storage actionRewards
) internal {
uint256 guaranteedRewardsLength = guaranteedRewards.length;
if (guaranteedRewardsLength != 0) {
actionRewards.guaranteedRewardTokenId1 = guaranteedRewards[0].itemTokenId;
actionRewards.guaranteedRewardRate1 = guaranteedRewards[0].rate;
}
if (guaranteedRewardsLength > 1) {
actionRewards.guaranteedRewardTokenId2 = guaranteedRewards[1].itemTokenId;
actionRewards.guaranteedRewardRate2 = guaranteedRewards[1].rate;
require(
actionRewards.guaranteedRewardTokenId1 != actionRewards.guaranteedRewardTokenId2,
GuaranteedRewardsNoDuplicates()
);
}
if (guaranteedRewardsLength > 2) {
actionRewards.guaranteedRewardTokenId3 = guaranteedRewards[2].itemTokenId;
actionRewards.guaranteedRewardRate3 = guaranteedRewards[2].rate;
uint256 bounds = guaranteedRewardsLength - 1;
for (uint256 i; i < bounds; ++i) {
require(
guaranteedRewards[i].itemTokenId != guaranteedRewards[guaranteedRewardsLength - 1].itemTokenId,
GuaranteedRewardsNoDuplicates()
);
}
}
require(guaranteedRewardsLength <= 3, TooManyGuaranteedRewards());
}
// Random rewards have most common one first
function _setActionRandomRewards(
RandomReward[] calldata randomRewards,
ActionRewards storage actionRewards
) internal {
uint256 randomRewardsLength = randomRewards.length;
if (randomRewardsLength != 0) {
actionRewards.randomRewardTokenId1 = randomRewards[0].itemTokenId;
actionRewards.randomRewardChance1 = randomRewards[0].chance;
actionRewards.randomRewardAmount1 = randomRewards[0].amount;
}
if (randomRewardsLength > 1) {
actionRewards.randomRewardTokenId2 = randomRewards[1].itemTokenId;
actionRewards.randomRewardChance2 = randomRewards[1].chance;
actionRewards.randomRewardAmount2 = randomRewards[1].amount;
require(
actionRewards.randomRewardChance2 <= actionRewards.randomRewardChance1,
RandomRewardsMustBeInOrder(randomRewards[0].chance, randomRewards[1].chance)
);
require(actionRewards.randomRewardTokenId1 != actionRewards.randomRewardTokenId2, RandomRewardNoDuplicates());
}
if (randomRewardsLength > 2) {
actionRewards.randomRewardTokenId3 = randomRewards[2].itemTokenId;
actionRewards.randomRewardChance3 = randomRewards[2].chance;
actionRewards.randomRewardAmount3 = randomRewards[2].amount;
require(
actionRewards.randomRewardChance3 <= actionRewards.randomRewardChance2,
RandomRewardsMustBeInOrder(randomRewards[1].chance, randomRewards[2].chance)
);
uint256 bounds = randomRewardsLength - 1;
for (uint256 i; i < bounds; ++i) {
require(
randomRewards[i].itemTokenId != randomRewards[randomRewardsLength - 1].itemTokenId,
RandomRewardNoDuplicates()
);
}
}
if (randomRewards.length > 3) {
actionRewards.randomRewardTokenId4 = randomRewards[3].itemTokenId;
actionRewards.randomRewardChance4 = randomRewards[3].chance;
actionRewards.randomRewardAmount4 = randomRewards[3].amount;
require(
actionRewards.randomRewardChance4 <= actionRewards.randomRewardChance3,
RandomRewardsMustBeInOrder(randomRewards[2].chance, randomRewards[3].chance)
);
uint256 bounds = randomRewards.length - 1;
for (uint256 i; i < bounds; ++i) {
require(
randomRewards[i].itemTokenId != randomRewards[randomRewards.length - 1].itemTokenId,
RandomRewardNoDuplicates()
);
}
}
require(randomRewards.length <= 4, TooManyRandomRewards());
}
function _get16bitSlice(bytes memory b, uint256 index) internal pure returns (uint16) {
uint256 key = index * 2;
return uint16(b[key] | (bytes2(b[key + 1]) >> 8));
}
// Helper function to get random value between min and max (inclusive) for uint8
function _getRandomInRange8(uint8 minValue, uint8 maxValue, uint8 randomness) internal pure returns (uint8) {
if (maxValue <= minValue) {
return minValue;
}
uint8 range = maxValue - minValue + 1;
// Use modulo to get value in range and add minValue
return uint8((uint16(randomness) % uint16(range)) + uint16(minValue));
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Skill, Attire, CombatStyle, CombatStats} from "./misc.sol";
import {GuaranteedReward, RandomReward} from "./rewards.sol";
enum ActionQueueStrategy {
OVERWRITE,
APPEND,
KEEP_LAST_IN_PROGRESS
}
struct QueuedActionInput {
Attire attire;
uint16 actionId;
uint16 regenerateId; // Food (combat), maybe something for non-combat later
uint16 choiceId; // Melee/Ranged/Magic (combat), logs, ore (non-combat)
uint16 rightHandEquipmentTokenId; // Axe/Sword/bow, can be empty
uint16 leftHandEquipmentTokenId; // Shield, can be empty
uint24 timespan; // How long to queue the action for
uint8 combatStyle; // CombatStyle specific style of combat
uint40 petId; // id of the pet (can be empty)
}
struct QueuedAction {
uint16 actionId;
uint16 regenerateId; // Food (combat), maybe something for non-combat later
uint16 choiceId; // Melee/Ranged/Magic (combat), logs, ore (non-combat)
uint16 rightHandEquipmentTokenId; // Axe/Sword/bow, can be empty
uint16 leftHandEquipmentTokenId; // Shield, can be empty
uint24 timespan; // How long to queue the action for
uint24 prevProcessedTime; // How long the action has been processed for previously
uint24 prevProcessedXPTime; // How much XP has been gained for this action so far
uint64 queueId; // id of this queued action
bytes1 packed; // 1st bit is isValid (not used yet), 2nd bit is for hasPet (decides if the 2nd storage slot is read)
uint8 combatStyle;
uint24 reserved;
// Next storage slot
uint40 petId; // id of the pet (can be empty)
}
// This is only used as an input arg (and events)
struct ActionInput {
uint16 actionId;
ActionInfo info;
GuaranteedReward[] guaranteedRewards;
RandomReward[] randomRewards;
CombatStats combatStats;
}
struct ActionInfo {
uint8 skill;
bool actionChoiceRequired; // If true, then the user must choose an action choice
uint24 xpPerHour;
uint32 minXP;
uint24 numSpawned; // Mostly for combat, capped respawn rate for xp/drops. Per hour, base 10000
uint16 handItemTokenIdRangeMin; // Inclusive
uint16 handItemTokenIdRangeMax; // Inclusive
uint8 successPercent; // 0-100
uint8 worldLocation; // 0 is the main starting world
bool isFullModeOnly;
bool isAvailable;
uint16 questPrerequisiteId;
}
uint16 constant ACTIONCHOICE_MELEE_BASIC_SWORD = 1500;
uint16 constant ACTIONCHOICE_MAGIC_SHADOW_BLAST = 2000;
uint16 constant ACTIONCHOICE_RANGED_BASIC_BOW = 3000;
// Allows for 2, 4 or 8 hour respawn time
uint256 constant SPAWN_MUL = 1000;
uint256 constant RATE_MUL = 1000;
uint256 constant GUAR_MUL = 10; // Guaranteeded reward multiplier (1 decimal, allows for 2 hour action times)
uint256 constant MAX_QUEUEABLE_ACTIONS = 3; // Available slots to queue actions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./actions.sol";
import "./items.sol";
import "./misc.sol";
import "./players.sol";
import "./rewards.sol";
import "./quests.sol";
import "./promotions.sol";
import "./clans.sol";
import "./pets.sol";
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IBank} from "../interfaces/IBank.sol";
enum ClanRank {
NONE, // Not in a clan
COMMONER, // Member of the clan
SCOUT, // Invite and kick commoners
COLONEL, // Can launch attacks and assign combatants
TREASURER, // Can withdraw from bank
LEADER, // Can edit clan details
OWNER // Can do everything and transfer ownership
}
enum BattleResultEnum {
DRAW,
WIN,
LOSE
}
struct ClanBattleInfo {
uint40 lastClanIdAttackOtherClanIdCooldownTimestamp;
uint8 numReattacks;
uint40 lastOtherClanIdAttackClanIdCooldownTimestamp;
uint8 numReattacksOtherClan;
}
// Packed for gas efficiency
struct Vault {
bool claimed; // Only applies to the first one, if it's claimed without the second one being claimed
uint40 timestamp;
uint80 amount;
uint40 timestamp1;
uint80 amount1;
}
struct VaultClanInfo {
IBank bank;
uint96 totalBrushLocked;
// New storage slot
uint40 attackingCooldownTimestamp;
uint40 assignCombatantsCooldownTimestamp;
bool currentlyAttacking;
uint24 defendingVaultsOffset;
uint40 blockAttacksTimestamp;
uint8 blockAttacksCooldownHours;
bool isInMMRArray;
uint40 superAttackCooldownTimestamp;
uint64[] playerIds;
Vault[] defendingVaults; // Append only, and use defendingVaultsOffset to decide where the real start is
}
uint256 constant MAX_CLAN_COMBATANTS = 20;
uint256 constant CLAN_WARS_GAS_PRICE_WINDOW_SIZE = 4;
bool constant XP_EMITTED_ELSEWHERE = true;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
uint16 constant NONE = 0;
uint16 constant COMBAT_BASE = 2048;
// Melee
uint16 constant SWORD_BASE = COMBAT_BASE;
uint16 constant BRONZE_SWORD = SWORD_BASE;
// Woodcutting (2816 - 3071)
uint16 constant WOODCUTTING_BASE = 2816;
uint16 constant BRONZE_AXE = WOODCUTTING_BASE;
// Firemaking (3328 - 3583)
uint16 constant FIRE_BASE = 3328;
uint16 constant MAGIC_FIRE_STARTER = FIRE_BASE;
uint16 constant FIRE_MAX = FIRE_BASE + 255;
// Fishing (3072 - 3327)
uint16 constant FISHING_BASE = 3072;
uint16 constant NET_STICK = FISHING_BASE;
// Mining (2560 - 2815)
uint16 constant MINING_BASE = 2560;
uint16 constant BRONZE_PICKAXE = MINING_BASE;
// Magic
uint16 constant STAFF_BASE = COMBAT_BASE + 50;
uint16 constant TOTEM_STAFF = STAFF_BASE;
// Ranged
uint16 constant BOW_BASE = COMBAT_BASE + 100;
uint16 constant BASIC_BOW = BOW_BASE;
// Cooked fish
uint16 constant COOKED_FISH_BASE = 11008;
uint16 constant COOKED_FEOLA = COOKED_FISH_BASE + 3;
// Scrolls
uint16 constant SCROLL_BASE = 12032;
uint16 constant SHADOW_SCROLL = SCROLL_BASE;
// Boosts
uint16 constant BOOST_BASE = 12800;
uint16 constant COMBAT_BOOST = BOOST_BASE;
uint16 constant XP_BOOST = BOOST_BASE + 1;
uint16 constant GATHERING_BOOST = BOOST_BASE + 2;
uint16 constant SKILL_BOOST = BOOST_BASE + 3;
uint16 constant ABSENCE_BOOST = BOOST_BASE + 4;
uint16 constant LUCKY_POTION = BOOST_BASE + 5;
uint16 constant LUCK_OF_THE_DRAW = BOOST_BASE + 6;
uint16 constant PRAY_TO_THE_BEARDIE = BOOST_BASE + 7;
uint16 constant PRAY_TO_THE_BEARDIE_2 = BOOST_BASE + 8;
uint16 constant PRAY_TO_THE_BEARDIE_3 = BOOST_BASE + 9;
uint16 constant BOOST_RESERVED_1 = BOOST_BASE + 10;
uint16 constant BOOST_RESERVED_2 = BOOST_BASE + 11;
uint16 constant BOOST_RESERVED_3 = BOOST_BASE + 12;
uint16 constant GO_OUTSIDE = BOOST_BASE + 13;
uint16 constant RAINING_RARES = BOOST_BASE + 14;
uint16 constant CLAN_BOOSTER = BOOST_BASE + 15;
uint16 constant CLAN_BOOSTER_2 = BOOST_BASE + 16;
uint16 constant CLAN_BOOSTER_3 = BOOST_BASE + 17;
uint16 constant BOOST_RESERVED_4 = BOOST_BASE + 18;
uint16 constant BOOST_RESERVED_5 = BOOST_BASE + 19;
uint16 constant BOOST_RESERVED_6 = BOOST_BASE + 20;
uint16 constant BOOST_MAX = 13055;
// Eggs
uint16 constant EGG_BASE = 12544;
uint16 constant SECRET_EGG_1_TIER1 = EGG_BASE;
uint16 constant SECRET_EGG_2_TIER1 = EGG_BASE + 1;
uint16 constant EGG_MAX = 12799;
// Miscs
uint16 constant MISC_BASE = 65535;
uint16 constant RAID_PASS = MISC_BASE - 1;
struct BulkTransferInfo {
uint256[] tokenIds;
uint256[] amounts;
address to;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
enum BoostType {
NONE,
ANY_XP,
COMBAT_XP,
NON_COMBAT_XP,
GATHERING,
ABSENCE,
PASSIVE_SKIP_CHANCE,
// Clan wars
PVP_BLOCK,
PVP_REATTACK,
PVP_SUPER_ATTACK,
// Combat stats
COMBAT_FIXED
}
struct Equipment {
uint16 itemTokenId;
uint24 amount;
}
enum Skill {
NONE,
COMBAT, // This is a helper which incorporates all combat skills, attack <-> magic, defence, health etc
MELEE,
RANGED,
MAGIC,
DEFENCE,
HEALTH,
RESERVED_COMBAT,
MINING,
WOODCUTTING,
FISHING,
SMITHING,
THIEVING,
CRAFTING,
COOKING,
FIREMAKING,
FARMING,
ALCHEMY,
FLETCHING,
FORGING,
RESERVED2,
RESERVED3,
RESERVED4,
RESERVED5,
RESERVED6,
RESERVED7,
RESERVED8,
RESERVED9,
RESERVED10,
RESERVED11,
RESERVED12,
RESERVED13,
RESERVED14,
RESERVED15,
RESERVED16,
RESERVED17,
RESERVED18,
RESERVED19,
RESERVED20,
TRAVELING // Helper Skill for travelling
}
struct Attire {
uint16 head;
uint16 neck;
uint16 body;
uint16 arms;
uint16 legs;
uint16 feet;
uint16 ring;
uint16 reserved1;
}
struct CombatStats {
// From skill points
int16 meleeAttack;
int16 magicAttack;
int16 rangedAttack;
int16 health;
// These include equipment
int16 meleeDefence;
int16 magicDefence;
int16 rangedDefence;
}
enum CombatStyle {
NONE,
ATTACK,
DEFENCE
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Skill} from "./misc.sol";
enum PetSkin {
NONE,
DEFAULT,
OG,
ONEKIN,
FROST,
CRYSTAL,
ANNIV1,
KRAGSTYR
}
enum PetEnhancementType {
NONE,
MELEE,
MAGIC,
RANGED,
DEFENCE,
HEALTH,
MELEE_AND_DEFENCE,
MAGIC_AND_DEFENCE,
RANGED_AND_DEFENCE
}
struct Pet {
Skill skillEnhancement1;
uint8 skillFixedEnhancement1;
uint8 skillPercentageEnhancement1;
Skill skillEnhancement2;
uint8 skillFixedEnhancement2;
uint8 skillPercentageEnhancement2;
uint40 lastAssignmentTimestamp;
address owner; // Will be used as an optimization to avoid having to look up the owner of the pet in another storage slot
bool isTransferable;
// New storage slot
uint24 baseId;
// These are used when training a pet
uint40 lastTrainedTimestamp;
uint8 skillFixedEnhancementMax1; // The maximum possible value for skillFixedEnhancement1 when training
uint8 skillFixedEnhancementMax2;
uint8 skillPercentageEnhancementMax1;
uint8 skillPercentageEnhancementMax2;
uint64 xp;
}
struct BasePetMetadata {
string description;
uint8 tier;
PetSkin skin;
PetEnhancementType enhancementType;
Skill skillEnhancement1;
uint8 skillFixedMin1;
uint8 skillFixedMax1;
uint8 skillFixedIncrement1;
uint8 skillPercentageMin1;
uint8 skillPercentageMax1;
uint8 skillPercentageIncrement1;
uint8 skillMinLevel1;
Skill skillEnhancement2;
uint8 skillFixedMin2;
uint8 skillFixedMax2;
uint8 skillFixedIncrement2;
uint8 skillPercentageMin2;
uint8 skillPercentageMax2;
uint8 skillPercentageIncrement2;
uint8 skillMinLevel2;
uint16 fixedStarThreshold;
uint16 percentageStarThreshold;
bool isTransferable;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {QueuedAction} from "./actions.sol";
import {Skill, BoostType, CombatStats, Equipment} from "./misc.sol";
import {PlayerQuest} from "./quests.sol";
// 4 bytes for each level. 0x00000000 is the first level, 0x00000054 is the second, etc.
bytes constant XP_BYTES = hex"0000000000000054000000AE0000010E00000176000001E60000025E000002DE00000368000003FD0000049B00000546000005FC000006C000000792000008730000096400000A6600000B7B00000CA400000DE100000F36000010A200001229000013CB0000158B0000176B0000196E00001B9400001DE20000205A000022FF000025D5000028DD00002C1E00002F99000033540000375200003B9A000040300000451900004A5C00004FFF0000560900005C810000637000006ADD000072D100007B570000847900008E42000098BE0000A3F90000B0020000BCE70000CAB80000D9860000E9630000FA6200010C990001201D0001350600014B6F0001637300017D2E000198C10001B64E0001D5F80001F7E600021C430002433B00026CFD000299BE0002C9B30002FD180003342B00036F320003AE730003F23D00043AE3000488BE0004DC2F0005359B000595700005FC2400066A360006E02D00075E990007E6160008774C000912EB0009B9B4000A6C74000B2C06000BF956000CD561000DC134000EBDF3000FCCD40010EF2400122648001373BF0014D9230016582C0017F2B00019AAA9001B8234001D7B95001F99390021DDBC00244BE60026E6B60029B15F002CAF51002FE43A0033540D00370303003AF5A4003F30CC0043B9B0004895E3004DCB600053609100595C53005FC6030066A585006E034D0075E86C007E5E980087703B0091287D009B935300A6BD8F00B2B4EE00BF882800CD470500DC026F00EBCC8500FCB8B7010EDBD5";
uint256 constant MAX_LEVEL = 140; // Original max level
uint256 constant MAX_LEVEL_1 = 160; // TODO: Update later
uint256 constant MAX_LEVEL_2 = 190; // TODO: Update later
enum EquipPosition {
NONE,
HEAD,
NECK,
BODY,
ARMS,
LEGS,
FEET,
RING,
SPARE2,
LEFT_HAND,
RIGHT_HAND,
BOTH_HANDS,
QUIVER,
MAGIC_BAG,
FOOD,
AUX, // wood, seeds etc..
BOOST_VIAL,
EXTRA_BOOST_VIAL,
GLOBAL_BOOST_VIAL,
CLAN_BOOST_VIAL,
PASSIVE_BOOST_VIAL,
LOCKED_VAULT,
TERRITORY
}
struct Player {
uint40 currentActionStartTimestamp; // The in-progress start time of the first queued action
Skill currentActionProcessedSkill1; // The skill that the queued action has already gained XP in
uint24 currentActionProcessedXPGained1; // The amount of XP that the queued action has already gained
Skill currentActionProcessedSkill2;
uint24 currentActionProcessedXPGained2;
Skill currentActionProcessedSkill3;
uint24 currentActionProcessedXPGained3;
uint16 currentActionProcessedFoodConsumed;
uint16 currentActionProcessedBaseInputItemsConsumedNum; // e.g scrolls, crafting materials etc
Skill skillBoosted1; // The first skill that is boosted
Skill skillBoosted2; // The second skill that is boosted (if applicable)
uint48 totalXP;
uint16 totalLevel; // Doesn't not automatically add new skills to it
bytes1 packedData; // Contains worldLocation in first 6 bits (0 is the main starting randomnessBeacon), and full mode unlocked in the upper most bit
// TODO: Can be up to 7
QueuedAction[] actionQueue;
string name; // Raw name
}
struct Item {
EquipPosition equipPosition;
bytes1 packedData; // 0x1 exists, upper most bit is full mode
uint16 questPrerequisiteId;
// Can it be transferred?
bool isTransferable; // TODO: Move into packedData
// Food
uint16 healthRestored;
// Boost vial
BoostType boostType;
uint16 boostValue; // Varies, could be the % increase
uint24 boostDuration; // How long the effect of the boost last
// Combat stats
int16 meleeAttack;
int16 magicAttack;
int16 rangedAttack;
int16 meleeDefence;
int16 magicDefence;
int16 rangedDefence;
int16 health;
// Minimum requirements in this skill to use this item (can be NONE)
Skill skill;
uint32 minXP;
}
// Used for events
struct BoostInfo {
uint40 startTime;
uint24 duration;
uint16 value;
uint16 itemTokenId; // Get the effect of it
BoostType boostType;
}
struct PlayerBoostInfo {
uint40 startTime;
uint24 duration;
uint16 value;
uint16 itemTokenId; // Get the effect of it
BoostType boostType;
// Another boost slot (for global/clan boosts this is the "last", for users it is the "extra")
uint40 extraOrLastStartTime;
uint24 extraOrLastDuration;
uint16 extraOrLastValue;
uint16 extraOrLastItemTokenId;
BoostType extraOrLastBoostType;
uint40 cooldown; // Just put here for packing
}
// This is effectively a ratio to produce 1 of outputTokenId.
// Available choices that can be undertaken for an action
struct ActionChoiceInput {
uint8 skill; // Skill that this action choice is related to
uint24 rate; // Rate of output produced per hour (base 1000) 3 decimals
uint24 xpPerHour;
uint16[] inputTokenIds;
uint24[] inputAmounts;
uint16 outputTokenId;
uint8 outputAmount;
uint8 successPercent; // 0-100
uint16 handItemTokenIdRangeMin; // Inclusive
uint16 handItemTokenIdRangeMax; // Inclusive
bool isFullModeOnly;
bool isAvailable;
uint16 questPrerequisiteId;
uint8[] skills; // Skills required to do this action choice
uint32[] skillMinXPs; // Min XP in the corresponding skills to be able to do this action choice
int16[] skillDiffs; // How much the skill is increased/decreased by this action choice
}
struct ActionChoice {
uint8 skill; // Skill that this action choice is related to
uint24 rate; // Rate of output produced per hour (base 1000) 3 decimals
uint24 xpPerHour;
uint16 inputTokenId1;
uint24 inputAmount1;
uint16 inputTokenId2;
uint24 inputAmount2;
uint16 inputTokenId3;
uint24 inputAmount3;
uint16 outputTokenId;
uint8 outputAmount;
uint8 successPercent; // 0-100
uint8 skill1; // Skills required to do this action choice, commonly the same as skill
uint32 skillMinXP1; // Min XP in the skill to be able to do this action choice
int16 skillDiff1; // How much the skill is increased/decreased by this action choice
uint8 skill2;
uint32 skillMinXP2;
int16 skillDiff2;
uint8 skill3;
uint32 skillMinXP3;
int16 skillDiff3;
uint16 handItemTokenIdRangeMin; // Inclusive
uint16 handItemTokenIdRangeMax; // Inclusive
uint16 questPrerequisiteId;
// FullMode is last bit, first 6 bits is worldLocation,
// 2nd last bit is if there are other skills in next storage slot to check,
// 3rd last bit if the input amounts should be used
bytes1 packedData;
}
// Must be in the same order as Skill enum
struct PackedXP {
uint40 melee;
uint40 ranged;
uint40 magic;
uint40 defence;
uint40 health;
uint40 reservedCombat;
bytes2 packedDataIsMaxed; // 2 bits per skill to indicate whether the maxed skill is reached. I think this was added in case we added a new max level which a user had already passed so old & new levels are the same and it would not trigger a level up event.
// Next slot
uint40 mining;
uint40 woodcutting;
uint40 fishing;
uint40 smithing;
uint40 thieving;
uint40 crafting;
bytes2 packedDataIsMaxed1; // 2 bits per skill to indicate whether the maxed skill is reached
// Next slot
uint40 cooking;
uint40 firemaking;
uint40 farming;
uint40 alchemy;
uint40 fletching;
uint40 forging;
bytes2 packedDataIsMaxed2; // 2 bits per skill to indicate whether the maxed skill is reached
}
struct AvatarInfo {
string name;
string description;
string imageURI;
Skill[2] startSkills; // Can be NONE
}
struct PastRandomRewardInfo {
uint16 itemTokenId;
uint24 amount;
uint64 queueId;
}
struct PendingQueuedActionEquipmentState {
uint256[] consumedItemTokenIds;
uint256[] consumedAmounts;
uint256[] producedItemTokenIds;
uint256[] producedAmounts;
}
struct PendingQueuedActionMetadata {
uint32 xpGained; // total xp gained
uint32 rolls;
bool died;
uint16 actionId;
uint64 queueId;
uint24 elapsedTime;
uint24 xpElapsedTime;
uint8 checkpoint;
}
struct PendingQueuedActionData {
// The amount of XP that the queued action has already gained
Skill skill1;
uint24 xpGained1;
Skill skill2; // Most likely health
uint24 xpGained2;
Skill skill3; // Could come
uint24 xpGained3;
// How much food is consumed in the current action so far
uint16 foodConsumed;
// How many base consumables are consumed in the current action so far
uint16 baseInputItemsConsumedNum;
}
struct PendingQueuedActionProcessed {
// XP gained during this session
Skill[] skills;
uint32[] xpGainedSkills;
// Data for the current action which has been previously processed, this is used to store on the Player
PendingQueuedActionData currentAction;
}
struct QuestState {
uint256[] consumedItemTokenIds;
uint256[] consumedAmounts;
uint256[] rewardItemTokenIds;
uint256[] rewardAmounts;
PlayerQuest[] activeQuestInfo;
uint256[] questsCompleted;
Skill[] skills; // Skills gained XP in
uint32[] xpGainedSkills; // XP gained in these skills
}
struct LotteryWinnerInfo {
uint16 lotteryId;
uint24 raffleId;
uint16 itemTokenId;
uint16 amount;
bool instantConsume;
uint64 playerId;
}
struct PendingQueuedActionState {
// These 2 are in sync. Separated to reduce gas/deployment costs as these are passed down many layers.
PendingQueuedActionEquipmentState[] equipmentStates;
PendingQueuedActionMetadata[] actionMetadatas;
QueuedAction[] remainingQueuedActions;
PastRandomRewardInfo[] producedPastRandomRewards;
uint256[] xpRewardItemTokenIds;
uint256[] xpRewardAmounts;
uint256[] dailyRewardItemTokenIds;
uint256[] dailyRewardAmounts;
PendingQueuedActionProcessed processedData;
bytes32 dailyRewardMask;
QuestState quests;
uint256 numPastRandomRewardInstancesToRemove;
uint8 worldLocation;
LotteryWinnerInfo lotteryWinner;
}
struct FullAttireBonusInput {
Skill skill;
uint8 bonusXPPercent;
uint8 bonusRewardsPercent; // 3 = 3%
uint16[5] itemTokenIds; // 0 = head, 1 = body, 2 arms, 3 body, 4 = feet
}
// Contains everything you need to create an item
struct ItemInput {
CombatStats combatStats;
uint16 tokenId;
EquipPosition equipPosition;
bool isTransferable;
bool isFullModeOnly;
bool isAvailable;
uint16 questPrerequisiteId;
// Minimum requirements in this skill
Skill skill;
uint32 minXP;
// Food
uint16 healthRestored;
// Boost
BoostType boostType;
uint16 boostValue; // Varies, could be the % increase
uint24 boostDuration; // How long the effect of the boost vial last
// uri
string metadataURI;
string name;
}
/* Order head, neck, body, arms, legs, feet, ring, reserved1,
leftHandEquipment, rightHandEquipment,
Not used yet: input1, input2,input3, regenerate, reserved2, reserved3 */
struct CheckpointEquipments {
uint16[16] itemTokenIds;
uint16[16] balances;
}
struct ActivePlayerInfo {
uint64 playerId;
uint40 checkpoint;
uint24 timespan;
uint24 timespan1;
uint24 timespan2;
}
uint8 constant START_LEVEL = 17; // Needs updating when there is a new skill. Only useful for new heroes.
uint256 constant MAX_UNIQUE_TICKETS = 64;
// Used in a bunch of places
uint256 constant IS_FULL_MODE_BIT = 7;
// Passive/Instant/InstantVRF/Actions/ActionChoices/Item action
uint256 constant IS_AVAILABLE_BIT = 6;
// Passive actions
uint256 constant HAS_RANDOM_REWARDS_BIT = 5;
// The rest use world location for first 4 bits
// Queued action
uint256 constant HAS_PET_BIT = 2;
uint256 constant IS_VALID_BIT = 1;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
enum Promotion {
NONE,
STARTER,
HALLOWEEN_2023,
XMAS_2023,
HALLOWEEN_2024,
HOLIDAY4, // Just have placeholders for now
HOLIDAY5,
HOLIDAY6,
HOLIDAY7,
HOLIDAY8,
HOLIDAY9,
HOLIDAY10
}
enum PromotionMintStatus {
NONE,
SUCCESS,
PROMOTION_ALREADY_CLAIMED,
ORACLE_NOT_CALLED,
MINTING_OUTSIDE_AVAILABLE_DATE,
PLAYER_DOES_NOT_QUALIFY,
PLAYER_NOT_HIT_ENOUGH_CLAIMS_FOR_STREAK_BONUS,
DEPENDENT_QUEST_NOT_COMPLETED
}
struct PromotionInfoInput {
Promotion promotion;
uint40 startTime;
uint40 endTime; // Exclusive
uint8 numDailyRandomItemsToPick; // Number of items to pick
uint40 minTotalXP; // Minimum xp required to claim
uint256 tokenCost; // Cost in brush to start the promotion, max 16mil
// Special promotion specific (like 1kin)
uint8 redeemCodeLength; // Length of the redeem code
bool adminOnly; // Only admins can mint the promotion, like for 1kin (Not used yet)
bool promotionTiedToUser; // If the promotion is tied to a user
bool promotionTiedToPlayer; // If the promotion is tied to the player
bool promotionMustOwnPlayer; // Must own the player to get the promotion
// Evolution specific
bool evolvedHeroOnly; // Only allow evolved heroes to claim
// Multiday specific
bool isMultiday; // The promotion is multi-day
uint256 brushCostMissedDay; // Cost in brush to mint the promotion if they miss a day (in ether), max 25.6 (base 100)
uint8 numDaysHitNeededForStreakBonus; // How many days to hit for the streak bonus
uint8 numDaysClaimablePeriodStreakBonus; // If there is a streak bonus, how many days to claim it after the promotion ends. If no final day bonus, set to 0
uint8 numRandomStreakBonusItemsToPick1; // Number of items to pick for the streak bonus
uint8 numRandomStreakBonusItemsToPick2; // Number of random items to pick for the streak bonus
uint16[] randomStreakBonusItemTokenIds1;
uint32[] randomStreakBonusAmounts1;
uint16[] randomStreakBonusItemTokenIds2;
uint32[] randomStreakBonusAmounts2;
uint16[] guaranteedStreakBonusItemTokenIds;
uint16[] guaranteedStreakBonusAmounts;
// Single and multiday
uint16[] guaranteedItemTokenIds; // Guaranteed items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] guaranteedAmounts; // Corresponding amounts to the itemTokenIds
uint16[] randomItemTokenIds; // Possible items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] randomAmounts; // Corresponding amounts to the randomItemTokenIds
// Quests
uint16 questPrerequisiteId;
}
struct PromotionInfo {
Promotion promotion;
uint40 startTime;
uint8 numDays;
uint8 numDailyRandomItemsToPick; // Number of items to pick
uint40 minTotalXP; // Minimum xp required to claim
uint24 tokenCost; // Cost in brush to mint the promotion (in ether), max 16mil
// Quests
uint16 questPrerequisiteId;
// Special promotion specific (like 1kin), could pack these these later
uint8 redeemCodeLength; // Length of the redeem code
bool adminOnly; // Only admins can mint the promotion, like for 1kin
bool promotionTiedToUser; // If the promotion is tied to a user
bool promotionTiedToPlayer; // If the promotion is tied to the player
bool promotionMustOwnPlayer; // Must own the player to get the promotion
// Evolution specific
bool evolvedHeroOnly; // Only allow evolved heroes to claim
// Multiday specific
bool isMultiday; // The promotion is multi-day
uint8 brushCostMissedDay; // Cost in brush to mint the promotion if they miss a day (in ether), max 25.5, base 100
uint8 numDaysHitNeededForStreakBonus; // How many days to hit for the streak bonus
uint8 numDaysClaimablePeriodStreakBonus; // If there is a streak bonus, how many days to claim it after the promotion ends. If no final day bonus, set to 0
uint8 numRandomStreakBonusItemsToPick1; // Number of items to pick for the streak bonus
uint8 numRandomStreakBonusItemsToPick2; // Number of random items to pick for the streak bonus
// Misc
uint16[] randomStreakBonusItemTokenIds1;
uint32[] randomStreakBonusAmounts1;
uint16[] randomStreakBonusItemTokenIds2; // Not used yet
uint32[] randomStreakBonusAmounts2; // Not used yet
uint16[] guaranteedStreakBonusItemTokenIds; // Not used yet
uint16[] guaranteedStreakBonusAmounts; // Not used yet
// Single and multiday
uint16[] guaranteedItemTokenIds; // Guaranteed items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] guaranteedAmounts; // Corresponding amounts to the itemTokenIds
uint16[] randomItemTokenIds; // Possible items for the promotions each day, if empty then they are handled in a specific way for the promotion like daily rewards
uint32[] randomAmounts; // Corresponding amounts to the randomItemTokenIds
}
uint256 constant BRUSH_COST_MISSED_DAY_MUL = 10;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Skill} from "./misc.sol";
struct QuestInput {
uint16 dependentQuestId; // The quest that must be completed before this one can be started
uint16 actionId1; // action to do
uint16 actionNum1; // how many (up to 65535)
uint16 actionId2; // another action to do
uint16 actionNum2; // how many (up to 65535)
uint16 actionChoiceId; // actionChoice to perform
uint16 actionChoiceNum; // how many to do (base number), (up to 65535)
Skill skillReward; // The skill to reward XP to
uint24 skillXPGained; // The amount of XP to give (up to 65535)
uint16 rewardItemTokenId1; // Reward an item
uint16 rewardAmount1; // amount of the reward (up to 65535)
uint16 rewardItemTokenId2; // Reward another item
uint16 rewardAmount2; // amount of the reward (up to 65535)
uint16 burnItemTokenId; // Burn an item
uint16 burnAmount; // amount of the burn (up to 65535)
uint16 questId; // Unique id for this quest
bool isFullModeOnly; // If true this quest requires the user be evolved
uint8 worldLocation; // 0 is the main starting world
}
struct Quest {
uint16 dependentQuestId; // The quest that must be completed before this one can be started
uint16 actionId1; // action to do
uint16 actionNum1; // how many (up to 65535)
uint16 actionId2; // another action to do
uint16 actionNum2; // how many (up to 65535)
uint16 actionChoiceId; // actionChoice to perform
uint16 actionChoiceNum; // how many to do (base number), (up to 65535)
Skill skillReward; // The skill to reward XP to
uint24 skillXPGained; // The amount of XP to give (up to 65535)
uint16 rewardItemTokenId1; // Reward an item
uint16 rewardAmount1; // amount of the reward (up to 65535)
uint16 rewardItemTokenId2; // Reward another item
uint16 rewardAmount2; // amount of the reward (up to 65535)
uint16 burnItemTokenId; // Burn an item
uint16 burnAmount; // amount of the burn (up to 65535)
uint16 reserved; // Reserved for future use (previously was questId and cleared)
bytes1 packedData; // FullMode is last bit, first 6 bits is worldLocation
}
struct PlayerQuest {
uint32 questId;
uint16 actionCompletedNum1;
uint16 actionCompletedNum2;
uint16 actionChoiceCompletedNum;
uint16 burnCompletedAmount;
}
uint256 constant QUEST_PURSE_STRINGS = 5; // MAKE SURE THIS MATCHES definitions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {BoostType, Equipment} from "./misc.sol";
struct GuaranteedReward {
uint16 itemTokenId;
uint16 rate; // num per hour (base 10, 1 decimal) for actions and num per duration for passive actions
}
struct RandomReward {
uint16 itemTokenId;
uint16 chance; // out of 65535
uint8 amount; // out of 255
}
struct PendingRandomReward {
uint16 actionId;
uint40 startTime;
uint24 xpElapsedTime;
uint16 boostItemTokenId;
uint24 elapsedTime;
uint40 boostStartTime; // When the boost was started
uint24 sentinelElapsedTime;
// Full equipment at the time this was generated
uint8 fullAttireBonusRewardsPercent;
uint64 queueId; // TODO: Could reduce this if more stuff is needed
}
struct ActionRewards {
uint16 guaranteedRewardTokenId1;
uint16 guaranteedRewardRate1; // Num per hour base 10 (1 decimal) for actions (Max 6553.5 per hour), num per duration for passive actions
uint16 guaranteedRewardTokenId2;
uint16 guaranteedRewardRate2;
uint16 guaranteedRewardTokenId3;
uint16 guaranteedRewardRate3;
// Random chance rewards
uint16 randomRewardTokenId1;
uint16 randomRewardChance1; // out of 65535
uint8 randomRewardAmount1; // out of 255
uint16 randomRewardTokenId2;
uint16 randomRewardChance2;
uint8 randomRewardAmount2;
uint16 randomRewardTokenId3;
uint16 randomRewardChance3;
uint8 randomRewardAmount3;
uint16 randomRewardTokenId4;
uint16 randomRewardChance4;
uint8 randomRewardAmount4;
// No more room in this storage slot!
}
struct XPThresholdReward {
uint32 xpThreshold;
Equipment[] rewards;
}
enum InstantVRFActionType {
NONE,
GENERIC,
FORGING,
EGG
}
struct InstantVRFActionInput {
uint16 actionId;
uint16[] inputTokenIds;
uint24[] inputAmounts;
bytes data;
InstantVRFActionType actionType;
bool isFullModeOnly;
bool isAvailable;
uint16 questPrerequisiteId;
}
struct InstantVRFRandomReward {
uint16 itemTokenId;
uint16 chance; // out of 65535
uint16 amount; // out of 65535
}
uint256 constant MAX_GUARANTEED_REWARDS_PER_ACTION = 3;
uint256 constant MAX_RANDOM_REWARDS_PER_ACTION = 4;
uint256 constant MAX_REWARDS_PER_ACTION = MAX_GUARANTEED_REWARDS_PER_ACTION + MAX_RANDOM_REWARDS_PER_ACTION;
uint256 constant MAX_CONSUMED_PER_ACTION = 3;
uint256 constant MAX_QUEST_REWARDS = 2;
uint256 constant TIER_1_DAILY_REWARD_START_XP = 0;
uint256 constant TIER_2_DAILY_REWARD_START_XP = 7_650;
uint256 constant TIER_3_DAILY_REWARD_START_XP = 33_913;
uint256 constant TIER_4_DAILY_REWARD_START_XP = 195_864;
uint256 constant TIER_5_DAILY_REWARD_START_XP = 784_726;
uint256 constant TIER_6_DAILY_REWARD_START_XP = 2_219_451;
// 4 bytes for each threshold, starts at 500 xp in decimal
bytes constant XP_THRESHOLD_REWARDS = hex"00000000000001F4000003E8000009C40000138800002710000075300000C350000186A00001D4C0000493E0000557300007A120000927C0000B71B0000DBBA0000F424000124F800016E360001B7740001E8480002625A0002932E0002DC6C0003567E0003D0900004C4B40005B8D80006ACFC0007A1200008954400098968000A7D8C000B71B0000C65D4000D59F8000E4E1C0";
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IBrushToken is IERC20 {
function burn(uint256 amount) external;
function burnFrom(address account, uint256 amount) external;
function transferFromBulk(address from, address[] calldata tos, uint256[] calldata amounts) external;
function transferOwnership(address newOwner) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface IMarketplaceWhitelist {
function isWhitelisted(address nft) external view returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface IBank {
function initialize() external;
function initializeAddresses(
uint256 clanId,
address bankRegistry,
address bankRelay,
address playerNFT,
address itemNFT,
address clans,
address players,
address lockedBankVaults,
address raids
) external;
function depositToken(address sender, address from, uint256 playerId, address token, uint256 amount) external;
function setAllowBreachedCapacity(bool allow) external;
}
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface IBankFactory {
function getBankAddress(uint256 clanId) external view returns (address);
function getCreatedHere(address bank) external view returns (bool);
function createBank(address from, uint256 clanId) external returns (address);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface IClanMemberLeftCB {
function clanMemberLeft(uint256 clanId, uint256 playerId) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {ClanRank} from "../globals/clans.sol";
interface IClans {
function canWithdraw(uint256 clanId, uint256 playerId) external view returns (bool);
function isClanMember(uint256 clanId, uint256 playerId) external view returns (bool);
function maxBankCapacity(uint256 clanId) external view returns (uint16);
function maxMemberCapacity(uint256 clanId) external view returns (uint16);
function getRank(uint256 clanId, uint256 playerId) external view returns (ClanRank);
function setMMR(uint256 clanId, uint16 mmr) external;
function getMMR(uint256 clanId) external view returns (uint16);
function addXP(uint256 clanId, uint40 xp, bool xpEmittedElsewhere) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "../globals/misc.sol";
import "../globals/players.sol";
interface IPlayers {
function clearEverythingBeforeTokenTransfer(address from, uint256 tokenId) external;
function beforeTokenTransferTo(address to, uint256 tokenId) external;
function getURI(
uint256 playerId,
string calldata name,
string calldata avatarName,
string calldata avatarDescription,
string calldata imageURI
) external view returns (string memory);
function mintedPlayer(
address from,
uint256 playerId,
Skill[2] calldata startSkills,
bool makeActive,
uint256[] calldata startingItemTokenIds,
uint256[] calldata startingAmounts
) external;
function upgradePlayer(uint256 playerId) external;
function isPlayerEvolved(uint256 playerId) external view returns (bool);
function isOwnerOfPlayerAndActive(address from, uint256 playerId) external view returns (bool);
function getAlphaCombatParams() external view returns (uint8 alphaCombat, uint8 betaCombat, uint8 alphaCombatHealing);
function getActivePlayer(address owner) external view returns (uint256 playerId);
function getPlayerXP(uint256 playerId, Skill skill) external view returns (uint256 xp);
function getLevel(uint256 playerId, Skill skill) external view returns (uint256 level);
function getTotalXP(uint256 playerId) external view returns (uint256 totalXP);
function getTotalLevel(uint256 playerId) external view returns (uint256 totalLevel);
function getActiveBoost(uint256 playerId) external view returns (PlayerBoostInfo memory);
function modifyXP(address from, uint256 playerId, Skill skill, uint56 xp, bool skipEffects) external;
function beforeItemNFTTransfer(address from, address to, uint256[] calldata ids, uint256[] calldata amounts) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/utils/structs/BitMaps.sol";
library BloomFilter {
using BitMaps for BitMaps.BitMap;
struct Filter {
uint8 hashCount; // Number of hash functions to use
uint64 bitCount; // Number of bits in the bitmap
BitMaps.BitMap bitmap; // Bitmap using OpenZeppelin’s BitMaps library to support up to 65,536 bits
}
error ZeroHashCount();
/**
* @notice Calculates the optimal number of hash functions based on the expected number of items.
* @param expectedItems Expected number of items to be added to the filter.
* @param bitCount Number of bits in the bitmap.
* @return hashCount The number of hash functions to be used.
*/
function _getOptimalHashCount(uint256 expectedItems, uint64 bitCount) internal pure returns (uint8 hashCount) {
uint256 calculatedHashCount = (bitCount * 144) / (expectedItems * 100) + 1;
hashCount = calculatedHashCount < 256 ? uint8(calculatedHashCount) : 255;
}
/**
* @notice Adds a `bytes32` item to the filter by setting bits in the bitmap.
* @param filter The Bloom filter to update.
* @param item Hash value of the item to add.
*/
function _add(Filter storage filter, bytes32 item) internal {
require(filter.hashCount != 0, ZeroHashCount());
uint64 bitCount = filter.bitCount;
for (uint8 i = 0; i < filter.hashCount; ++i) {
uint256 position = uint256(keccak256(abi.encodePacked(item, i))) % bitCount;
filter.bitmap.set(position); // Set the bit in the bitmap at the calculated position
}
}
/**
* @notice Adds a string to the filter by hashing it and setting bits in the bitmap.
* @param filter The Bloom filter to update.
* @param item String to add to the filter.
*/
function _addString(Filter storage filter, string memory item) internal {
bytes32 itemHash = keccak256(abi.encodePacked(item));
_add(filter, itemHash);
}
/**
* @notice Removes a `bytes32` item from the filter by clearing bits in the bitmap.
* @param filter The Bloom filter to update.
* @param item Hash value of the item to remove.
*/
function _remove(Filter storage filter, bytes32 item) internal {
require(filter.hashCount != 0, ZeroHashCount());
uint64 bitCount = filter.bitCount;
for (uint8 i = 0; i < filter.hashCount; ++i) {
uint256 position = uint256(keccak256(abi.encodePacked(item, i))) % bitCount;
filter.bitmap.unset(position); // Clear the bit in the bitmap at the calculated position
}
}
/**
* @notice Removes a string from the filter by hashing it and clearing bits in the bitmap.
* @param filter The Bloom filter to update.
* @param item String to remove from the filter.
*/
function _removeString(Filter storage filter, string memory item) internal {
bytes32 itemHash = keccak256(abi.encodePacked(item));
_remove(filter, itemHash);
}
/**
* @notice Checks if a `bytes32` item is probably present in the filter or definitely not present.
* @param filter The Bloom filter to check.
* @param item Hash value of the item to check.
* @return probablyPresent True if the item may exist, false if it definitely does not exist.
*/
function _probablyContains(Filter storage filter, bytes32 item) internal view returns (bool probablyPresent) {
if (filter.hashCount == 0) revert ZeroHashCount();
uint64 bitCount = filter.bitCount;
for (uint8 i = 0; i < filter.hashCount; ++i) {
uint256 position = uint256(keccak256(abi.encodePacked(item, i))) % bitCount;
if (!filter.bitmap.get(position)) return false; // If any bit is not set, item is not present
}
return true;
}
/**
* @notice Checks if a string is probably present in the filter or definitely not present.
* @param filter The Bloom filter to check.
* @param item String to check in the filter.
* @return probablyPresent True if the item may exist, false if it definitely does not exist.
*/
function _probablyContainsString(
Filter storage filter,
string memory item
) internal view returns (bool probablyPresent) {
bytes32 itemHash = keccak256(abi.encodePacked(item));
return _probablyContains(filter, itemHash);
}
function _defaults(Filter storage filter) internal {
filter.hashCount = 8; // The number of hash functions to use.
filter.bitCount = 1024 * 32; // Default number of bits
delete filter.bitmap; // Clear the bitmap
}
/**
* @notice Initializes a Bloom filter with a specified hash count.
* @param filter The Bloom filter to initialize.
*/
function _initialize(Filter storage filter) internal {
_defaults(filter);
}
/**
* @notice Initializes a Bloom filter with a specified hash count.
* @param filter The Bloom filter to initialize.
* @param hashCount The number of hash functions to use.
*/
function _initialize(Filter storage filter, uint8 hashCount) internal {
_defaults(filter);
filter.hashCount = hashCount;
}
/**
* @notice Initializes a Bloom filter with a specified hash count.
* @param filter The Bloom filter to initialize.
* @param hashCount The number of hash functions to use.
* @param bitCount The number of bits in the bitmap.
*/
function _initialize(Filter storage filter, uint8 hashCount, uint64 bitCount) internal {
_defaults(filter);
filter.bitCount = bitCount;
filter.hashCount = hashCount;
}
/**
* @notice Initializes a Bloom filter with a specified hash count and clears the bitmap.
* @param filter The Bloom filter to initialize.
* @param hashCount The times to hash each item.
* @param positions Array of positions to set in the bitmap.
*/
function _initialize(Filter storage filter, uint8 hashCount, uint256[] calldata positions) internal {
_initialize(filter, hashCount);
_addPositions(filter, positions);
}
/**
* @notice Initializes a Bloom filter with a specified hash count and clears the bitmap.
* @param filter The Bloom filter to initialize.
* @param hashCount The number of hash functions to use.
* @param bitCount The number of bits in the bitmap.
* @param positions Array of positions to set in the bitmap.
*/
function _initialize(Filter storage filter, uint8 hashCount, uint64 bitCount, uint256[] calldata positions) internal {
_initialize(filter, hashCount, bitCount);
_addPositions(filter, positions);
}
/**
* @notice Adds an array of positions to the filter by setting bits in the bitmap.
* @param filter The Bloom filter to update.
* @param positions Array of positions to set in the bitmap.
*/
function _addPositions(Filter storage filter, uint256[] calldata positions) internal {
for (uint256 i = 0; i < positions.length; ++i) {
filter.bitmap.set(positions[i]);
}
}
}