diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af13eaa..8c3b2d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,3 +73,9 @@ function test_claim_revert_invalidDistribution() public { // ... } ``` + +## Archiving Previous Contract Versions + +To maintain a clear version history and ensure traceability of contract changes, we need to archive previous versions of smart contracts rather than deleting or overwriting them. +We use to archive a contract: [`forge flatten src/your-contract-name > archive/your-contract-name`](https://book.getfoundry.sh/forge/fmt/). “Flattening” a smart contracts basically copies the contract and all its dependencies into a single file. +Example: `forge flatten src/FANtiumAthletesV10.sol > src/archive/FANtiumAthletesV10.sol` Note: this should be done before you make any changes to the contract. diff --git a/script/DeployTestnet.s.sol b/script/DeployTestnet.s.sol index 4254c85..3ce13bb 100644 --- a/script/DeployTestnet.s.sol +++ b/script/DeployTestnet.s.sol @@ -5,7 +5,7 @@ import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Script } from "forge-std/Script.sol"; -import { FANtiumAthletesV10 } from "src/FANtiumAthletesV10.sol"; +import { FANtiumAthletesV11 } from "src/FANtiumAthletesV11.sol"; import { FANtiumClaimingV5 } from "src/FANtiumClaimingV5.sol"; import { FANtiumMarketplaceV1 } from "src/FANtiumMarketplaceV1.sol"; import { FANtiumTokenV1 } from "src/FANtiumTokenV1.sol"; @@ -35,9 +35,9 @@ contract DeployTestnet is Script { function run() public { vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); - FANtiumAthletesV10 fantiumAthletes = FANtiumAthletesV10( + FANtiumAthletesV11 fantiumAthletes = FANtiumAthletesV11( UnsafeUpgrades.deployUUPSProxy( - address(new FANtiumAthletesV10()), abi.encodeCall(FANtiumAthletesV10.initialize, (ADMIN)) + address(new FANtiumAthletesV11()), abi.encodeCall(FANtiumAthletesV11.initialize, (ADMIN)) ) ); diff --git a/script/UpgradeMainnetV10.s.sol b/script/UpgradeMainnetV10.s.sol index ede4c53..0da9dca 100644 --- a/script/UpgradeMainnetV10.s.sol +++ b/script/UpgradeMainnetV10.s.sol @@ -5,7 +5,7 @@ import { Options } from "@openzeppelin/foundry-upgrades/Options.sol"; import { Core } from "@openzeppelin/foundry-upgrades/internal/Core.sol"; import { Script } from "forge-std/Script.sol"; -contract UpgradeMainnetV10 is Script { +contract UpgradeMainnetV11 is Script { error OnlyPolygonMainnet(); function run() public { @@ -17,7 +17,7 @@ contract UpgradeMainnetV10 is Script { vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); Options memory opts; - Core.prepareUpgrade("FANtiumAthletesV10.sol:FANtiumAthletesV10", opts); + Core.prepareUpgrade("FANtiumAthletesV11.sol:FANtiumAthletesV11", opts); Core.prepareUpgrade("FANtiumClaimingV5.sol:FANtiumClaimingV5", opts); vm.stopBroadcast(); } diff --git a/script/UpgradeTestnet.s.sol b/script/UpgradeTestnet.s.sol index 9870c6f..ce432ac 100644 --- a/script/UpgradeTestnet.s.sol +++ b/script/UpgradeTestnet.s.sol @@ -36,7 +36,7 @@ contract UpgradeTestnet is Script { vm.createSelectFork(vm.rpcUrl("amoy")); vm.startBroadcast(vm.envUint("ADMIN_PRIVATE_KEY")); if (FANTIUM_ATHLETES_UPGRADE) { - Upgrades.upgradeProxy(FANTIUM_ATHLETES_PROXY, "FANtiumAthletesV10.sol:FANtiumAthletesV10", ""); + Upgrades.upgradeProxy(FANTIUM_ATHLETES_PROXY, "FANtiumAthletesV11.sol:FANtiumAthletesV11", ""); } if (FANTIUM_CLAIMING_UPGRADE) { diff --git a/src/FANtiumAthletesV10.sol b/src/FANtiumAthletesV11.sol similarity index 90% rename from src/FANtiumAthletesV10.sol rename to src/FANtiumAthletesV11.sol index 29fd6a4..98a14d5 100644 --- a/src/FANtiumAthletesV10.sol +++ b/src/FANtiumAthletesV11.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; +import { IFANtiumAthletes, MintRequest, VerificationStatus } from "./interfaces/IFANtiumAthletes.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -15,6 +16,9 @@ import { } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import { StringsUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; import { Collection, CollectionData, @@ -27,18 +31,19 @@ import { Rescue } from "src/utils/Rescue.sol"; import { TokenVersionUtil } from "src/utils/TokenVersionUtil.sol"; /** - * @title FANtium Athletes ERC721 contract V10. + * @title FANtium Athletes ERC721 contract V11. * @author Mathieu Bour, Alex Chernetsky - FANtium AG, based on previous work by MTX studio AG. - * @custom:oz-upgrades-from src/archive/FANtiumAthletesV9.sol:FANtiumAthletesV9 + * @custom:oz-upgrades-from src/archive/FANtiumAthletesV10.sol:FANtiumAthletesV10 */ -contract FANtiumAthletesV10 is +contract FANtiumAthletesV11 is Initializable, ERC721Upgradeable, UUPSUpgradeable, AccessControlUpgradeable, PausableUpgradeable, Rescue, - IFANtiumAthletes + IFANtiumAthletes, + EIP712 { using StringsUpgradeable for uint256; using ECDSAUpgradeable for bytes32; @@ -71,6 +76,10 @@ contract FANtiumAthletesV10 is */ bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + /// @notice EIP-712 typehash for KYC status struct + bytes32 public constant VERIFICATION_STATUS_TYPEHASH = + keccak256("VerificationStatus(address account,uint8 level,uint256 expiresAt)"); + // ======================================================================== // State variables // ======================================================================== @@ -153,11 +162,14 @@ contract FANtiumAthletesV10 is __UUPSUpgradeable_init(); __AccessControl_init(); __Pausable_init(); - _grantRole(DEFAULT_ADMIN_ROLE, admin); nextCollectionId = 1; } + function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { + return (NAME, "11"); + } + /** * @notice Implementation of the upgrade authorization logic * @dev Restricted to the DEFAULT_ADMIN_ROLE @@ -575,44 +587,55 @@ contract FANtiumAthletesV10 is emit Sale(collectionId, quantity, recipient, amount, discount); } + // ======================================================================== /** - * @notice Purchase NFTs from the sale. - * @param collectionId The collection ID to purchase from. - * @param quantity The quantity of NFTs to purchase. - * @param recipient The recipient of the NFTs. + * @dev Verifies the KYC status signature + * @param verificationStatus The KYC status to verify + * @param signature The backend-generated signature for user purchasing the athlete NFT */ - function mintTo(uint256 collectionId, uint24 quantity, address recipient) public whenNotPaused returns (uint256) { - uint256 amount = _expectedPrice(collectionId, quantity); - return _mintTo(collectionId, quantity, amount, recipient); + function _verifySignature(VerificationStatus calldata verificationStatus, bytes calldata signature) internal view { + bytes32 kycStatusHash = keccak256( + abi.encode( + VERIFICATION_STATUS_TYPEHASH, + verificationStatus.account, + verificationStatus.level, + verificationStatus.expiresAt + ) + ); + + bytes32 digest = _hashTypedData(kycStatusHash); + address signer = ECDSA.recover(digest, signature); + + if (!hasRole(SIGNER_ROLE, signer)) { + revert InvalidMint(MintErrorReason.INVALID_SIGNATURE); + } } /** - * @notice Purchase NFTs from the sale with a custom price, checked - * @param collectionId The collection ID to purchase from. - * @param quantity The quantity of NFTs to purchase. - * @param recipient The recipient of the NFTs. - * @param amount The amount of tokens to purchase the NFTs with. - * @param signature The signature of the purchase request. + * @notice Purchase NFTs from the sale. + * @param mintRequest All the data required for purchase: collectionId, quantity, recipient etc. + * @param signature The backend-generated signature for user purchasing the athlete NFT */ function mintTo( - uint256 collectionId, - uint24 quantity, - address recipient, - uint256 amount, - bytes memory signature + MintRequest calldata mintRequest, + bytes calldata signature ) - public + external whenNotPaused returns (uint256) { - bytes32 hash = - keccak256(abi.encode(collectionId, quantity, recipient, amount, nonces[recipient])).toEthSignedMessageHash(); - if (!hasRole(SIGNER_ROLE, hash.recover(signature))) { - revert InvalidMint(MintErrorReason.INVALID_SIGNATURE); + _verifySignature(mintRequest.verificationStatus, signature); + + // purchase requires AML check (level 1) + if (mintRequest.verificationStatus.level < 1) { + revert InvalidMint(MintErrorReason.ACCOUNT_NOT_KYCED); } - nonces[recipient]++; - return _mintTo(collectionId, quantity, amount, recipient); + if (mintRequest.verificationStatus.expiresAt < block.timestamp) { + revert InvalidMint(MintErrorReason.SIGNATURE_EXPIRED); + } + // todo: we do NOT include amount + quantity into signature. Security vulnerability ? + return _mintTo(mintRequest.collectionId, mintRequest.quantity, mintRequest.amount, mintRequest.recipient); } // ======================================================================== diff --git a/src/archive/FANtiumAthletesV10.sol b/src/archive/FANtiumAthletesV10.sol new file mode 100644 index 0000000..98d4d53 --- /dev/null +++ b/src/archive/FANtiumAthletesV10.sol @@ -0,0 +1,4143 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.28 ^0.8.0 ^0.8.1 ^0.8.2; + +// node_modules/@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol) + +/** + * @dev External interface of AccessControl declared to support ERC165 detection. + */ +interface IAccessControlUpgradeable { + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {AccessControl-_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) external view returns (bool); + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {AccessControl-_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external; + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) external; +} + +// node_modules/@openzeppelin/contracts-upgradeable/interfaces/IERC1967Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) + +/** + * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. + * + * _Available since v4.8.3._ + */ +interface IERC1967Upgradeable { + /** + * @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); +} + +// node_modules/@openzeppelin/contracts-upgradeable/interfaces/draft-IERC1822Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) + +/** + * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * proxy whose upgrades are fully controlled by the current implementation. + */ +interface IERC1822ProxiableUpgradeable { + /** + * @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); +} + +// node_modules/@openzeppelin/contracts-upgradeable/proxy/beacon/IBeaconUpgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeaconUpgradeable { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {BeaconProxy} will check that this address is a contract. + */ + function implementation() external view returns (address); +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20Upgradeable { + /** + * @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 amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.4) (token/ERC20/extensions/IERC20Permit.sol) + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20PermitUpgradeable { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol) + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721ReceiverUpgradeable { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be + * reverted. + * + * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) + external + returns (bytes4); +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) + +/** + * @dev Collection of functions related to the address type + */ +library AddressUpgradeable { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * + * Furthermore, `isContract` will also return true if the target contract within + * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, + * which only has an effect at the end of a transaction. + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @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.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions + * pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success,) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @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, it is bubbled up by this + * function (like regular Solidity function calls). + * + * 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. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) + internal + returns (bytes memory) + { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @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`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) + internal + returns (bytes memory) + { + require(address(this).balance >= value, "Address: insufficient balance for call"); + (bool success, bytes memory returndata) = target.call{ value: value }(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) + internal + view + returns (bytes memory) + { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) + internal + returns (bytes memory) + { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) + internal + view + returns (bytes memory) + { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) + internal + pure + returns (bytes memory) + { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) 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 + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/StorageSlotUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +/** + * @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 ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = + * 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ + */ +library StorageSlotUpgradeable { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 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) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + 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) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + 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) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165Upgradeable { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library MathUpgradeable { + enum Rounding { + Down, // Toward negative infinity + Up, // Toward infinity + Zero // Toward zero + + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds up instead + * of rounding down. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0 + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) + * with further edits by Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + require(denominator > prod1, "Math: mulDiv overflow"); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always + // >= 1. + // See https://cs.stackexchange.com/q/138556/92363. + + // Does not overflow because the denominator cannot be zero at this stage in the function. + uint256 twos = denominator & (~denominator + 1); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works + // in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256, rounded down, of a positive value. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0); + } + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/math/SignedMathUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMathUpgradeable { + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // must be unchecked in order to support `n = type(int256).min` + return uint256(n >= 0 ? n : -n); + } + } +} + +// src/interfaces/IRescue.sol + +/** + * @title IRescue + * @notice Interface for rescuing NFTs in emergency situations + * @dev This interface provides functionality for authorized parties to transfer NFTs to a specified address + */ +interface IRescue { + /** + * @notice Emitted when a token is rescued + * @param tokenId The ID of the rescued token + * @param recipient The address that received the rescued token + * @param reason A string explaining why the token was rescued + */ + event Rescued(uint256 tokenId, address recipient, string reason); + + /** + * @notice Rescues a single token by transferring it to a specified address + * @param tokenId The ID of the token to rescue + * @param reason A string explaining why the token is being rescued + */ + function rescue(uint256 tokenId, string memory reason) external; + + /** + * @notice Rescues multiple tokens by transferring them to a specified address + * @param tokenIds An array of token IDs to rescue + * @param reason A string explaining why the tokens are being rescued + */ + function rescueBatch(uint256[] memory tokenIds, string memory reason) external; +} + +// src/utils/TokenVersionUtil.sol + +/** + * @title Claiming contract that allows payout tokens to be claimed + * for FAN token holders. + * @author FAANtium AG - based onMTX stuido AG. + */ +library TokenVersionUtil { + uint256 private constant ONE_MILLION = 1_000_000; + uint256 private constant TEN_THOUSAND = 10_000; + uint256 public constant MAX_VERSION = 99; + + function getTokenInfo(uint256 tokenId) + internal + pure + returns (uint256 collectionId, uint256 version, uint256 number, uint256 baseTokenId) + { + collectionId = tokenId / ONE_MILLION; + version = (tokenId % ONE_MILLION) / TEN_THOUSAND; + number = tokenId % TEN_THOUSAND; + baseTokenId = collectionId * ONE_MILLION + number; + } + + function createTokenId( + uint256 _collectionId, + uint256 _versionId, + uint256 _tokenNr + ) + internal + pure + returns (uint256) + { + uint256 tokenId = (_collectionId * ONE_MILLION) + (_versionId * TEN_THOUSAND) + _tokenNr; + + return (tokenId); + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) + +/** + * @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 Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 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 functions marked with `initializer` can be nested in the context of a + * constructor. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _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 255 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _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() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @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 { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized != type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint8) { + return _initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _initializing; + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20MetadataUpgradeable is IERC20Upgradeable { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol) + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721Upgradeable is IERC165Upgradeable { + /** + * @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 ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must 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 ERC721 + * 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 caller. + * + * 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); +} + +// src/utils/Rescue.sol + +/** + * @title Rescue + * @author Mathieu Bour - FANtium AG + * @notice Utility contract for rescuing tokens from the contract + * @dev This contract is intended to be used as an internal utility for contracts that need to rescue tokens from the + * contract. It is not intended to be used as a standalone contract. + */ +abstract contract Rescue is IRescue { + /** + * @notice Authorizes a rescue of a token by checking if the sender has the DEFAULT_ADMIN_ROLE + * @param tokenId The ID of the token to rescue + * @param recipient The address that received the rescued token + * @param reason A string explaining why the token is being rescued + */ + function _authorizeRescue(uint256 tokenId, address recipient, string calldata reason) internal virtual; + + /** + * @notice Rescues a single token by transferring it to a specified address + * @param tokenId The ID of the token to rescue + * @param recipient The address that received the rescued token + * @param reason A string explaining why the token was rescued + */ + function _rescue(uint256 tokenId, address recipient, string calldata reason) internal virtual; + + /** + * @notice Rescues a single token by transferring it to a specified address + * @param tokenId The ID of the token to rescue + * @param recipient The address that received the rescued token + * @param reason A string explaining why the token was rescued + */ + function _doRescue(uint256 tokenId, address recipient, string calldata reason) internal { + _authorizeRescue(tokenId, recipient, reason); + _rescue(tokenId, recipient, reason); + emit Rescued(tokenId, recipient, reason); + } + + /** + * @notice Rescues a single token by transferring it to a specified address + * @param tokenId The ID of the token to rescue + * @param reason A string explaining why the token is being rescued + */ + function rescue(uint256 tokenId, string calldata reason) external { + _doRescue(tokenId, msg.sender, reason); + } + + /** + * @notice Rescues multiple tokens by transferring them to a specified address + * @param tokenIds An array of token IDs to rescue + * @param reason A string explaining why the tokens are being rescued + */ + function rescueBatch(uint256[] memory tokenIds, string calldata reason) external { + for (uint256 i = 0; i < tokenIds.length; i++) { + _doRescue(tokenIds[i], msg.sender, reason); + } + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol) + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721MetadataUpgradeable is IERC721Upgradeable { + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.4) (utils/Context.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; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) + +/** + * @dev String operations. + */ +library StringsUpgradeable { + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = MathUpgradeable.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) + } + value /= 10; + if (value == 0) { + break; + } + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toString(int256 value) internal pure returns (string memory) { + return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMathUpgradeable.abs(value)))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, MathUpgradeable.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal + * representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } +} + +// src/interfaces/IFANtiumAthletes.sol + +/** + * @notice Collection struct + * @dev CAUTION: Do not change the order of the struct fields!! + * + * Difference between isMintable and isPaused: + * - isMintable false means that nobody can mint new tokens + * - isPaused true means that the collection is mintable only by member of the collection allowlist + * + * price does not take the token decimals into account, which means that if the price is 1,000UDSC, + * mintTo function will need to multiply the price by 10^decimals of the token. + */ +struct Collection { + /** + * @notice Always true if the collection exists. + */ + bool exists; + /** + * @notice UNIX timestamp of the collection launch. + */ + uint256 launchTimestamp; + /** + * @notice True if the collection is mintable. + */ + bool isMintable; + /** + * @notice True if the collection is paused. + */ + bool isPaused; + /** + * @notice Number of minted tokens. + */ + uint24 invocations; + /** + * @notice Price of a token in the collection without decimals, which means that this price must be multiplied by + * 10^decimals of the token. + */ + uint256 price; + /** + * @notice Maximum number of tokens that can be minted. + */ + uint256 maxInvocations; + /** + * @notice Tournament earnings share in 1e7 basis points. + */ + uint256 tournamentEarningShare1e7; + /** + * @notice Address of the athlete. + */ + address payable athleteAddress; + /** + * @notice Athlete primary sales share in 10,000 basis points. + */ + uint256 athletePrimarySalesBPS; + /** + * @notice Athlete secondary sales share in 10,000 basis points. + */ + uint256 athleteSecondarySalesBPS; + /** + * @notice Address of the FANtium sales. + */ + address payable UNUSED_fantiumSalesAddress; + /** + * @notice FANtium secondary sales share in 10,000 basis points. + */ + uint256 fantiumSecondarySalesBPS; + /** + * @notice Other earnings (e.g. sponsorships, royalties, etc.) share in 1e7 basis points. + */ + uint256 otherEarningShare1e7; +} + +/** + * @notice Create collection struct + * @dev Fields may be added. + */ +struct CollectionData { + address payable athleteAddress; + uint256 athletePrimarySalesBPS; + uint256 athleteSecondarySalesBPS; + uint256 fantiumSecondarySalesBPS; + uint256 launchTimestamp; + uint256 maxInvocations; + uint256 otherEarningShare1e7; + uint256 price; + uint256 tournamentEarningShare1e7; +} + +enum CollectionErrorReason { + INVALID_BPS_SUM, + INVALID_MAX_INVOCATIONS, + INVALID_PRIMARY_SALES_BPS, + INVALID_SECONDARY_SALES_BPS, + MAX_COLLECTIONS_REACHED, + INVALID_TOURNAMENT_EARNING_SHARE, + INVALID_OTHER_EARNING_SHARE, + INVALID_ATHLETE_ADDRESS, + INVALID_FANTIUM_SALES_ADDRESS, + INVALID_PRICE +} + +enum MintErrorReason { + INVALID_COLLECTION_ID, + COLLECTION_NOT_MINTABLE, + COLLECTION_NOT_LAUNCHED, + COLLECTION_PAUSED, + ACCOUNT_NOT_KYCED, + INVALID_SIGNATURE +} + +enum UpgradeErrorReason { + INVALID_COLLECTION_ID, + VERSION_ID_TOO_HIGH +} + +interface IFANtiumAthletes is IERC721Upgradeable { + // ======================================================================== + // Events + // ======================================================================== + event CollectionCreated(uint256 indexed collectionId, Collection collection); + event CollectionUpdated(uint256 indexed collectionId, Collection collection); + event Sale( + uint256 indexed collectionId, uint24 quantity, address indexed recipient, uint256 amount, uint256 discount + ); + + // ======================================================================== + // Errors + // ======================================================================== + error InvalidCollectionId(uint256 collectionId); + error AthleteOnly(uint256 collectionId, address account, address expected); + error InvalidCollection(CollectionErrorReason reason); + error InvalidMint(MintErrorReason reason); + error InvalidUpgrade(UpgradeErrorReason reason); + + // ======================================================================== + // Collection + // ======================================================================== + function collections(uint256 collectionId) external view returns (Collection memory); + function createCollection(CollectionData memory data) external returns (uint256); + function updateCollection(uint256 collectionId, CollectionData memory data) external; + function setCollectionStatus(uint256 collectionId, bool isMintable, bool isPaused) external; + + // ======================================================================== + // Revenue splits + // ======================================================================== + function getPrimaryRevenueSplits( + uint256 _collectionId, + uint256 _price + ) + external + view + returns ( + uint256 fantiumRevenue_, + address payable fantiumAddress_, + uint256 athleteRevenue_, + address payable athleteAddress_ + ); + + // ======================================================================== + // Minting + // ======================================================================== + function mintTo(uint256 collectionId, uint24 quantity, address recipient) external returns (uint256); + + function mintTo( + uint256 collectionId, + uint24 quantity, + address recipient, + uint256 amount, + bytes memory signature + ) + external + returns (uint256); + + // ======================================================================== + // Claiming + // ======================================================================== + function upgradeTokenVersion(uint256 tokenId) external returns (uint256); +} + +// node_modules/@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) + +/** + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. + */ +abstract contract PausableUpgradeable is Initializable, ContextUpgradeable { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + bool private _paused; + + /** + * @dev Initializes the contract in unpaused state. + */ + function __Pausable_init() internal onlyInitializing { + __Pausable_init_unchained(); + } + + function __Pausable_init_unchained() internal onlyInitializing { + _paused = false; + } + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused() { + _requireNotPaused(); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused() { + _requirePaused(); + _; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); + } + + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.3) (token/ERC20/utils/SafeERC20.sol) + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20Upgradeable { + using AddressUpgradeable for address; + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20Upgradeable token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ + function safeTransferFrom(IERC20Upgradeable token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove(IERC20Upgradeable token, address spender, uint256 value) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeIncreaseAllowance(IERC20Upgradeable token, address spender, uint256 value) internal { + uint256 oldAllowance = token.allowance(address(this), spender); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value)); + } + + /** + * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeDecreaseAllowance(IERC20Upgradeable token, address spender, uint256 value) internal { + unchecked { + uint256 oldAllowance = token.allowance(address(this), spender); + require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value)); + } + } + + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval + * to be set to zero before setting it to a non-zero value, such as USDT. + */ + function forceApprove(IERC20Upgradeable token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0)); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`. + * Revert on invalid signature. + */ + function safePermit( + IERC20PermitUpgradeable token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + internal + { + uint256 nonceBefore = token.nonces(owner); + token.permit(owner, spender, value, deadline, v, r, s); + uint256 nonceAfter = token.nonces(owner); + require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed"); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20Upgradeable token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); + require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20Upgradeable token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return success && (returndata.length == 0 || abi.decode(returndata, (bool))) + && AddressUpgradeable.isContract(address(token)); + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSAUpgradeable { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS, + InvalidSignatureV // Deprecated in v4.8 + + } + + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature` or error string. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, signature); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + * + * _Available since v4.2._ + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); + } + + return (signer, RecoverError.NoError); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); + _throwError(error); + return recovered; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } + } + + /** + * @dev Returns an Ethereum Signed Message, created from `s`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", StringsUpgradeable.toString(s.length), s)); + } + + /** + * @dev Returns an Ethereum Signed Typed Data, created from a + * `domainSeparator` and a `structHash`. This produces hash corresponding + * to the one signed with the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] + * JSON-RPC method as part of EIP-712. + * + * See {recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } + } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); + } +} + +// node_modules/@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165Upgradeable is Initializable, IERC165Upgradeable { + function __ERC165_init() internal onlyInitializing { } + + function __ERC165_init_unchained() internal onlyInitializing { } + /** + * @dev See {IERC165-supportsInterface}. + */ + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165Upgradeable).interfaceId; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + */ +abstract contract ERC1967UpgradeUpgradeable is Initializable, IERC1967Upgradeable { + // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 + bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + function __ERC1967Upgrade_init() internal onlyInitializing { } + + function __ERC1967Upgrade_init_unchained() internal onlyInitializing { } + /** + * @dev Returns the current implementation address. + */ + + function _getImplementation() internal view returns (address) { + return StorageSlotUpgradeable.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + require(AddressUpgradeable.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlotUpgradeable.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {Upgraded} event. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + _upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + AddressUpgradeable.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { + // Upgrades from old implementations will perform a rollback test. This test requires the new + // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing + // this special case will break upgrade paths from old UUPS implementation to new ones. + if (StorageSlotUpgradeable.getBooleanSlot(_ROLLBACK_SLOT).value) { + _setImplementation(newImplementation); + } else { + try IERC1822ProxiableUpgradeable(newImplementation).proxiableUUID() returns (bytes32 slot) { + require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); + } catch { + revert("ERC1967Upgrade: new implementation is not UUPS"); + } + _upgradeToAndCall(newImplementation, data, forceCall); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Returns the current admin. + */ + function _getAdmin() internal view returns (address) { + return StorageSlotUpgradeable.getAddressSlot(_ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); + StorageSlotUpgradeable.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _changeAdmin(address newAdmin) internal { + emit AdminChanged(_getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. + */ + bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Returns the current beacon. + */ + function _getBeacon() internal view returns (address) { + return StorageSlotUpgradeable.getAddressSlot(_BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + require(AddressUpgradeable.isContract(newBeacon), "ERC1967: new beacon is not a contract"); + require( + AddressUpgradeable.isContract(IBeaconUpgradeable(newBeacon).implementation()), + "ERC1967: beacon implementation is not a contract" + ); + StorageSlotUpgradeable.getAddressSlot(_BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits a {BeaconUpgraded} event. + */ + function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + AddressUpgradeable.functionDelegateCall(IBeaconUpgradeable(newBeacon).implementation(), data); + } + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.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. + * + * _Available since v4.1._ + */ +abstract contract UUPSUpgradeable is Initializable, IERC1822ProxiableUpgradeable, ERC1967UpgradeUpgradeable { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment + address private immutable __self = address(this); + + /** + * @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 ERC1967) 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 ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to + * fail. + */ + modifier onlyProxy() { + require(address(this) != __self, "Function must be called through delegatecall"); + require(_getImplementation() == __self, "Function must be called through active proxy"); + _; + } + + /** + * @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() { + require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall"); + _; + } + + function __UUPSUpgradeable_init() internal onlyInitializing { } + + function __UUPSUpgradeable_init_unchained() internal onlyInitializing { } + /** + * @dev Implementation of the ERC1822 {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 override notDelegated returns (bytes32) { + return _IMPLEMENTATION_SLOT; + } + + /** + * @dev Upgrade the implementation of the proxy to `newImplementation`. + * + * Calls {_authorizeUpgrade}. + * + * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall + */ + function upgradeTo(address newImplementation) public virtual onlyProxy { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); + } + + /** + * @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, true); + } + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * + * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. + * + * ```solidity + * function _authorizeUpgrade(address) internal override onlyOwner {} + * ``` + */ + function _authorizeUpgrade(address newImplementation) internal virtual; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol) + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ```solidity + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ```solidity + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules} + * to enforce additional security measures for this role. + */ +abstract contract AccessControlUpgradeable is + Initializable, + ContextUpgradeable, + IAccessControlUpgradeable, + ERC165Upgradeable +{ + struct RoleData { + mapping(address => bool) members; + bytes32 adminRole; + } + + mapping(bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Modifier that checks that an account has a specific role. Reverts + * with a standardized message including the required role. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/ + * + * _Available since v4.1._ + */ + modifier onlyRole(bytes32 role) { + _checkRole(role); + _; + } + + function __AccessControl_init() internal onlyInitializing { } + + function __AccessControl_init_unchained() internal onlyInitializing { } + /** + * @dev See {IERC165-supportsInterface}. + */ + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAccessControlUpgradeable).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view virtual override returns (bool) { + return _roles[role].members[account]; + } + + /** + * @dev Revert with a standard message if `_msgSender()` is missing `role`. + * Overriding this function changes the behavior of the {onlyRole} modifier. + * + * Format of the revert message is described in {_checkRole}. + * + * _Available since v4.6._ + */ + function _checkRole(bytes32 role) internal view virtual { + _checkRole(role, _msgSender()); + } + + /** + * @dev Revert with a standard message if `account` is missing `role`. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/ + */ + function _checkRole(bytes32 role, address account) internal view virtual { + if (!hasRole(role, account)) { + revert( + string( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(account), + " is missing role ", + StringsUpgradeable.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleGranted} event. + */ + function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + * + * May emit a {RoleRevoked} event. + */ + function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been revoked `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + * + * May emit a {RoleRevoked} event. + */ + function renounceRole(bytes32 role, address account) public virtual override { + require(account == _msgSender(), "AccessControl: can only renounce roles for self"); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * May emit a {RoleGranted} event. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + * + * NOTE: This function is deprecated in favor of {_grantRole}. + */ + function _setupRole(bytes32 role, address account) internal virtual { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + bytes32 previousAdminRole = getRoleAdmin(role); + _roles[role].adminRole = adminRole; + emit RoleAdminChanged(role, previousAdminRole, adminRole); + } + + /** + * @dev Grants `role` to `account`. + * + * Internal function without access restriction. + * + * May emit a {RoleGranted} event. + */ + function _grantRole(bytes32 role, address account) internal virtual { + if (!hasRole(role, account)) { + _roles[role].members[account] = true; + emit RoleGranted(role, account, _msgSender()); + } + } + + /** + * @dev Revokes `role` from `account`. + * + * Internal function without access restriction. + * + * May emit a {RoleRevoked} event. + */ + function _revokeRole(bytes32 role, address account) internal virtual { + if (hasRole(role, account)) { + _roles[role].members[account] = false; + emit RoleRevoked(role, account, _msgSender()); + } + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} + +// node_modules/@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol) + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension, but not including the Enumerable extension, which is available separately as + * {ERC721Enumerable}. + */ +contract ERC721Upgradeable is + Initializable, + ContextUpgradeable, + ERC165Upgradeable, + IERC721Upgradeable, + IERC721MetadataUpgradeable +{ + using AddressUpgradeable for address; + using StringsUpgradeable for uint256; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to owner address + mapping(uint256 => address) private _owners; + + // Mapping owner address to token count + mapping(address => uint256) private _balances; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + function __ERC721_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC721_init_unchained(name_, symbol_); + } + + function __ERC721_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (ERC165Upgradeable, IERC165Upgradeable) + returns (bool) + { + return interfaceId == type(IERC721Upgradeable).interfaceId + || interfaceId == type(IERC721MetadataUpgradeable).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + require(owner != address(0), "ERC721: address zero is not a valid owner"); + return _balances[owner]; + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + address owner = _ownerOf(tokenId); + require(owner != address(0), "ERC721: invalid token ID"); + return owner; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + _requireMinted(tokenId); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overridden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721Upgradeable.ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require( + _msgSender() == owner || isApprovedForAll(owner, _msgSender()), + "ERC721: approve caller is not token owner or approved for all" + ); + + _approve(to, tokenId); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + _requireMinted(tokenId); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + //solhint-disable-next-line max-line-length + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + _safeTransfer(from, to, tokenId, data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * `data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon + * a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { + _transfer(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist + */ + function _ownerOf(uint256 tokenId) internal view virtual returns (address) { + return _owners[tokenId]; + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _ownerOf(tokenId) != address(0); + } + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { + address owner = ERC721Upgradeable.ownerOf(tokenId); + return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon + * a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { + _mint(to, tokenId); + require( + _checkOnERC721Received(address(0), to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId, 1); + + // Check that tokenId was not minted by `_beforeTokenTransfer` hook + require(!_exists(tokenId), "ERC721: token already minted"); + + unchecked { + // Will not overflow unless all 2**256 token ids are minted to the same owner. + // Given that tokens are minted one by one, it is impossible in practice that + // this ever happens. Might change if we allow batch minting. + // The ERC fails to describe this case. + _balances[to] += 1; + } + + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + + _afterTokenTransfer(address(0), to, tokenId, 1); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * This is an internal function that does not check if the sender is authorized to operate on the token. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + address owner = ERC721Upgradeable.ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId, 1); + + // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook + owner = ERC721Upgradeable.ownerOf(tokenId); + + // Clear approvals + delete _tokenApprovals[tokenId]; + + unchecked { + // Cannot overflow, as that would require more tokens to be burned/transferred + // out than the owner initially received through minting and transferring in. + _balances[owner] -= 1; + } + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + + _afterTokenTransfer(owner, address(0), tokenId, 1); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) internal virtual { + require(ERC721Upgradeable.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + require(to != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId, 1); + + // Check that tokenId was not transferred by `_beforeTokenTransfer` hook + require(ERC721Upgradeable.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + + // Clear approvals from the previous owner + delete _tokenApprovals[tokenId]; + + unchecked { + // `_balances[from]` cannot overflow for the same reason as described in `_burn`: + // `from`'s balance is the number of token held, which is at least one before the current + // transfer. + // `_balances[to]` could overflow in the conditions described in `_mint`. That would require + // all 2**256 token ids to be minted, which in practice is impossible. + _balances[from] -= 1; + _balances[to] += 1; + } + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId, 1); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits an {Approval} event. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _tokenApprovals[tokenId] = to; + emit Approval(ERC721Upgradeable.ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Emits an {ApprovalForAll} event. + */ + function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { + require(owner != operator, "ERC721: approve to caller"); + _operatorApprovals[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + /** + * @dev Reverts if the `tokenId` has not been minted yet. + */ + function _requireMinted(uint256 tokenId) internal view virtual { + require(_exists(tokenId), "ERC721: invalid token ID"); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory data + ) + private + returns (bool) + { + if (to.isContract()) { + try IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, data) returns ( + bytes4 retval + ) { + return retval == IERC721ReceiverUpgradeable.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`. + * - When `from` is zero, the tokens will be minted for `to`. + * - When `to` is zero, ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * - `batchSize` is non-zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual { } + + /** + * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`. + * - When `from` is zero, the tokens were minted for `to`. + * - When `to` is zero, ``from``'s tokens were burned. + * - `from` and `to` are never both zero. + * - `batchSize` is non-zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual { } + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant + * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such + * that `ownerOf(tokenId)` is `a`. + */ + // solhint-disable-next-line func-name-mixedcase + function __unsafe_increaseBalance(address account, uint256 amount) internal { + _balances[account] += amount; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[44] private __gap; +} + +// src/FANtiumAthletesV10.sol + +/** + * @title FANtium Athletes ERC721 contract V10. + * @author Mathieu Bour, Alex Chernetsky - FANtium AG, based on previous work by MTX studio AG. + * @custom:oz-upgrades-from src/archive/FANtiumAthletesV9.sol:FANtiumAthletesV9 + */ +contract FANtiumAthletesV10 is + Initializable, + ERC721Upgradeable, + UUPSUpgradeable, + AccessControlUpgradeable, + PausableUpgradeable, + Rescue, + IFANtiumAthletes +{ + using StringsUpgradeable for uint256; + using ECDSAUpgradeable for bytes32; + using SafeERC20Upgradeable for IERC20MetadataUpgradeable; + + // ======================================================================== + // Constants + // ======================================================================== + string private constant NAME = "FANtium"; + string private constant SYMBOL = "FAN"; + + uint256 private constant BPS_BASE = 10_000; + uint256 private constant MAX_COLLECTIONS = 1_000_000; + uint256 private constant MAX_INVOCATIONS = 10_000; + + // Roles + // ======================================================================== + bytes32 public constant FORWARDER_ROLE = keccak256("FORWARDER_ROLE"); + bytes32 public constant SIGNER_ROLE = keccak256("SIGNER_ROLE"); + + /** + * @notice Role for the token upgrader. + * @dev Used to upgrade the token to a new version. + */ + bytes32 public constant TOKEN_UPGRADER_ROLE = keccak256("TOKEN_UPGRADER_ROLE"); + + /** + * @notice Trusted operator role that can approve all transfers - only first party operators are allowed. + * @dev Used by the marketplace to approve transfers. + */ + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + // ======================================================================== + // State variables + // ======================================================================== + /** + * @notice Mapping of collection IDs to collection data. + * @custom:oz-retyped-from mapping(uint256 => Collection) + */ + mapping(uint256 => Collection) private _collections; + + /** + * @notice The base URI for the token metadata. + */ + string public baseURI; + + /** + * @notice Mapping of collection IDs to allowlist allocations. + * @dev Deprecated: replaced by the userManager contract. + */ + mapping(uint256 => mapping(address => uint256)) private UNUSED_collectionIdToAllowList; + + /** + * @notice Mapping of addresses that have been KYCed. + * @dev Deprecated: replaced by the userManager contract. + */ + mapping(address => bool) private UNUSED_kycedAddresses; + + /** + * @notice The next collection ID to be used. + */ + uint256 public nextCollectionId; + + /** + * @notice The ERC20 token used for payments, usually a stablecoin. + */ + IERC20MetadataUpgradeable public erc20PaymentToken; + + /** + * @dev Deprecated: replaced by the TOKEN_UPGRADER_ROLE. + */ + address private UNUSED_claimContract; + + /** + * @dev Deprecated: kept for upgrade compatibility + * @custom:oz-renamed-from userManager + */ + address private UNUSED_userManager; + + /** + * @dev Deprecated: replaced by the FORWARDER_ROLE. + */ + address private UNUSED_trustedForwarder; + + /** + * @notice Mapping of addresses to their nonce. + * @dev Used to prevent replay attacks with the mintTo function. + */ + mapping(address => uint256) public nonces; + + /** + * @dev The FANtium treasury address. + */ + address payable public treasury; + + // ======================================================================== + // UUPS upgradeable pattern + // ======================================================================== + /** + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes contract using the UUPS upgradeable pattern. + * @param admin The admin address. + */ + function initialize(address admin) public initializer { + __ERC721_init(NAME, SYMBOL); + __UUPSUpgradeable_init(); + __AccessControl_init(); + __Pausable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + nextCollectionId = 1; + } + + /** + * @notice Implementation of the upgrade authorization logic + * @dev Restricted to the DEFAULT_ADMIN_ROLE + */ + function _authorizeUpgrade(address) internal view override { + _checkRole(DEFAULT_ADMIN_ROLE); + } + + // ======================================================================== + // Access control + // ======================================================================== + modifier onlyAdmin() { + _checkRole(DEFAULT_ADMIN_ROLE); + _; + } + + // ======================================================================== + // Modifiers + // ======================================================================== + modifier onlyAthleteOrAdmin(uint256 collectionId) { + if (_msgSender() != _collections[collectionId].athleteAddress && !hasRole(DEFAULT_ADMIN_ROLE, _msgSender())) { + revert AthleteOnly(collectionId, _msgSender(), _collections[collectionId].athleteAddress); + } + _; + } + + modifier onlyValidCollectionId(uint256 _collectionId) { + if (!_collections[_collectionId].exists) { + revert InvalidCollectionId(_collectionId); + } + _; + } + + // ======================================================================== + // Pause + // ======================================================================== + /** + * @notice Update contract pause status to `_paused`. + */ + function pause() external onlyAdmin { + _pause(); + } + + /** + * @notice Unpauses contract + */ + function unpause() external onlyAdmin { + _unpause(); + } + + // ======================================================================== + // ERC2771 + // ======================================================================== + function isTrustedForwarder(address forwarder) public view virtual returns (bool) { + return hasRole(FORWARDER_ROLE, forwarder); + } + + function _msgSender() internal view virtual override returns (address sender) { + if (isTrustedForwarder(msg.sender)) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + /// @solidity memory-safe-assembly + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return super._msgSender(); + } + } + + function _msgData() internal view virtual override returns (bytes calldata) { + if (isTrustedForwarder(msg.sender)) { + return msg.data[:msg.data.length - 20]; + } else { + return super._msgData(); + } + } + + // ======================================================================== + // Interface + // ======================================================================== + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override (IERC165Upgradeable, AccessControlUpgradeable, ERC721Upgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + // ======================================================================== + // Setters + // ======================================================================== + /** + * @notice Sets the base URI for the token metadata. + * @dev Restricted to admin. + * @param baseURI_ The new base URI. + */ + function setBaseURI(string memory baseURI_) external whenNotPaused onlyAdmin { + baseURI = baseURI_; + } + + /** + * @notice Sets the ERC20 payment token. + * @dev Restricted to admin. + * @param _erc20PaymentToken The new ERC20 payment token. + */ + function setERC20PaymentToken(IERC20MetadataUpgradeable _erc20PaymentToken) external whenNotPaused onlyAdmin { + erc20PaymentToken = _erc20PaymentToken; + } + + /** + * @notice Sets the FANtium treasury address. + * @dev Restricted to admin. + * @param _treasury The new FANtium treasury address. + */ + function setTreasury(address payable _treasury) external whenNotPaused onlyAdmin { + treasury = _treasury; + } + + // ======================================================================== + // ERC721 + // ======================================================================== + /** + * @dev Returns the base URI for computing {tokenURI}. + * Necessary to use the default ERC721 tokenURI function from ERC721Upgradeable. + */ + function _baseURI() internal view virtual override returns (string memory) { + return baseURI; + } + + /** + * @notice Returns true if `operator` is allowed to manage all of `owner`'s assets. + * @dev First party operators are allowed to manage all assets without restrictions. + */ + function isApprovedForAll( + address owner, + address operator + ) + public + view + override (ERC721Upgradeable, IERC721Upgradeable) + returns (bool) + { + if (hasRole(OPERATOR_ROLE, operator)) { + return true; + } + + return super.isApprovedForAll(owner, operator); + } + + // ======================================================================== + // Collections + // ======================================================================== + function collections(uint256 _collectionId) external view returns (Collection memory) { + return _collections[_collectionId]; + } + + function _checkCollectionData(CollectionData memory data) internal view { + // Validate the data + if (data.athleteAddress == address(0)) { + revert InvalidCollection(CollectionErrorReason.INVALID_ATHLETE_ADDRESS); + } + + if (data.athletePrimarySalesBPS > BPS_BASE) { + revert InvalidCollection(CollectionErrorReason.INVALID_PRIMARY_SALES_BPS); + } + + if (data.athleteSecondarySalesBPS + data.fantiumSecondarySalesBPS > BPS_BASE) { + revert InvalidCollection(CollectionErrorReason.INVALID_BPS_SUM); + } + + if (data.maxInvocations >= MAX_INVOCATIONS) { + revert InvalidCollection(CollectionErrorReason.INVALID_MAX_INVOCATIONS); + } + + if (data.otherEarningShare1e7 > 1e7) { + revert InvalidCollection(CollectionErrorReason.INVALID_OTHER_EARNING_SHARE); + } + + // no check on the price + + if (data.tournamentEarningShare1e7 > 1e7) { + revert InvalidCollection(CollectionErrorReason.INVALID_TOURNAMENT_EARNING_SHARE); + } + + if (nextCollectionId >= MAX_COLLECTIONS) { + revert InvalidCollection(CollectionErrorReason.MAX_COLLECTIONS_REACHED); + } + } + + /** + * @notice Creates a new collection. + * @dev Restricted to admin. + * @param data The new collection data. + * @return collectionId The ID of the created collection. + */ + function createCollection(CollectionData memory data) external whenNotPaused onlyAdmin returns (uint256) { + _checkCollectionData(data); + + uint256 collectionId = nextCollectionId++; + Collection memory newCollection = Collection({ + athleteAddress: data.athleteAddress, + athletePrimarySalesBPS: data.athletePrimarySalesBPS, + athleteSecondarySalesBPS: data.athleteSecondarySalesBPS, + exists: true, + UNUSED_fantiumSalesAddress: payable(address(0)), + fantiumSecondarySalesBPS: data.fantiumSecondarySalesBPS, + invocations: 0, + isMintable: false, + isPaused: true, + launchTimestamp: data.launchTimestamp, + maxInvocations: data.maxInvocations, + otherEarningShare1e7: data.otherEarningShare1e7, + price: data.price, + tournamentEarningShare1e7: data.tournamentEarningShare1e7 + }); + _collections[collectionId] = newCollection; + emit CollectionCreated(collectionId, newCollection); + + return collectionId; + } + + /** + * @notice Updates a collection. + * @dev Restricted to admin. + * @param collectionId The collection ID to update. + * @param data The new collection data. + */ + function updateCollection( + uint256 collectionId, + CollectionData memory data + ) + external + onlyValidCollectionId(collectionId) + whenNotPaused + onlyAdmin + { + _checkCollectionData(data); + + // Ensure the max invocations is not decreased + Collection memory updatedCollection = _collections[collectionId]; + if (data.maxInvocations < updatedCollection.invocations) { + revert InvalidCollection(CollectionErrorReason.INVALID_MAX_INVOCATIONS); + } + + updatedCollection.athleteAddress = data.athleteAddress; + updatedCollection.athletePrimarySalesBPS = data.athletePrimarySalesBPS; + updatedCollection.athleteSecondarySalesBPS = data.athleteSecondarySalesBPS; + updatedCollection.fantiumSecondarySalesBPS = data.fantiumSecondarySalesBPS; + updatedCollection.launchTimestamp = data.launchTimestamp; + updatedCollection.maxInvocations = data.maxInvocations; + updatedCollection.otherEarningShare1e7 = data.otherEarningShare1e7; + updatedCollection.price = data.price; + updatedCollection.tournamentEarningShare1e7 = data.tournamentEarningShare1e7; + _collections[collectionId] = updatedCollection; + + emit CollectionUpdated(collectionId, updatedCollection); + } + + /** + * @notice Sets the mintable and paused state of collection `collectionId`. + * A non-mintable collection prevents any minting. + * A paused collection prevents regular accounts to mint tokens but allow members of the allowlist to mint. + * @dev Restricted to athlete or manager or admin. + * @param collectionId The collection ID to set the status of. + * @param isMintable The new mintable state of the collection. + * @param isPaused The new paused state of the collection. + */ + function setCollectionStatus( + uint256 collectionId, + bool isMintable, + bool isPaused + ) + external + whenNotPaused + onlyValidCollectionId(collectionId) + onlyAthleteOrAdmin(collectionId) + { + _collections[collectionId].isMintable = isMintable; + _collections[collectionId].isPaused = isPaused; + } + + // ======================================================================== + // Revenue splits + // ======================================================================== + /** + * @notice Returns the primary revenue splits for a given collection and amount. + * @dev The share formula is based on the BPS values set for the collection on a 10,000 basis. + * @param collectionId collection ID to be queried. + * @param amount The amount to share between the athlete and FANtium. + * @return fantiumRevenue amount of revenue to be sent to FANtium + * @return fantiumAddress address to send FANtium revenue to + * @return athleteRevenue amount of revenue to be sent to athlete + * @return athleteAddress address to send athlete revenue to + */ + function getPrimaryRevenueSplits( + uint256 collectionId, + uint256 amount + ) + public + view + returns ( + uint256 fantiumRevenue, + address payable fantiumAddress, + uint256 athleteRevenue, + address payable athleteAddress + ) + { + // get athlete address & revenue from collection + Collection memory collection = _collections[collectionId]; + + // calculate revenues + athleteRevenue = (amount * collection.athletePrimarySalesBPS) / BPS_BASE; + fantiumRevenue = amount - athleteRevenue; + + // set addresses from storage + fantiumAddress = treasury; + athleteAddress = collection.athleteAddress; + } + + /** + * @dev splits funds between sender (if refund), + * FANtium, and athlete for a token purchased on + * collection `_collectionId`. + */ + function _splitFunds(uint256 _price, uint256 _collectionId, address _sender) internal { + // split funds between FANtium and athlete + (uint256 fantiumRevenue_, address fantiumAddress_, uint256 athleteRevenue_, address athleteAddress_) = + getPrimaryRevenueSplits(_collectionId, _price); + + // FANtium payment + if (fantiumRevenue_ > 0) { + erc20PaymentToken.safeTransferFrom(_sender, fantiumAddress_, fantiumRevenue_); + } + + // athlete payment + if (athleteRevenue_ > 0) { + erc20PaymentToken.safeTransferFrom(_sender, athleteAddress_, athleteRevenue_); + } + } + + // ======================================================================== + // Minting + // ======================================================================== + /** + * @notice Checks if a mint is possible for a collection + * @param collectionId Collection ID. + */ + function mintable(uint256 collectionId) public view onlyValidCollectionId(collectionId) { + Collection memory collection = _collections[collectionId]; + if (!collection.isMintable) { + revert InvalidMint(MintErrorReason.COLLECTION_NOT_MINTABLE); + } + + if (collection.launchTimestamp > block.timestamp) { + revert InvalidMint(MintErrorReason.COLLECTION_NOT_LAUNCHED); + } + + // If the collection is paused, we need to check if the recipient is on the allowlist and has enough allocation + if (collection.isPaused) { + revert InvalidMint(MintErrorReason.COLLECTION_PAUSED); + } + } + + /** + * @notice Calculates the expected price in payment tokens for minting NFTs from a collection + * @dev Multiplies the collection's base price by quantity and adjusts for the payment token's decimals + * @param collectionId The ID of the collection to calculate the price for + * @param quantity The number of NFTs to be minted + * @return The total price in the smallest unit of the payment token (e.g., wei for ETH, cents for USDC) + */ + function _expectedPrice(uint256 collectionId, uint24 quantity) internal view returns (uint256) { + Collection memory collection = _collections[collectionId]; + return collection.price * quantity * 10 ** IERC20MetadataUpgradeable(erc20PaymentToken).decimals(); + } + + /** + * @dev Internal function to mint tokens of a collection. + * @param collectionId Collection ID. + * @param quantity Amount of tokens to mint. + * @param amount Amount of ERC20 tokens to pay for the mint. + * @param recipient Recipient of the mint. + * @return lastTokenId The ID of the last minted token. Token range is [lastTokenId - quantity + 1, lastTokenId]. + */ + function _mintTo( + uint256 collectionId, + uint24 quantity, + uint256 amount, + address recipient + ) + internal + whenNotPaused + returns (uint256 lastTokenId) + { + Collection memory collection = _collections[collectionId]; + uint256 tokenId = (collectionId * MAX_COLLECTIONS) + collection.invocations; + + mintable(collectionId); + + _collections[collectionId].invocations += quantity; + + // Send funds to the treasury and athlete account. + _splitFunds(amount, collectionId, _msgSender()); + + for (uint256 i = 0; i < quantity; i++) { + _mint(recipient, tokenId + i); + } + + lastTokenId = tokenId + quantity - 1; + + uint256 expectedPrice = _expectedPrice(collectionId, quantity); + // expectedPrice can theoretically be higher than paid amount + uint256 discount = expectedPrice >= amount ? expectedPrice - amount : 0; + emit Sale(collectionId, quantity, recipient, amount, discount); + } + + /** + * @notice Purchase NFTs from the sale. + * @param collectionId The collection ID to purchase from. + * @param quantity The quantity of NFTs to purchase. + * @param recipient The recipient of the NFTs. + */ + function mintTo(uint256 collectionId, uint24 quantity, address recipient) public whenNotPaused returns (uint256) { + uint256 amount = _expectedPrice(collectionId, quantity); + return _mintTo(collectionId, quantity, amount, recipient); + } + + /** + * @notice Purchase NFTs from the sale with a custom price, checked + * @param collectionId The collection ID to purchase from. + * @param quantity The quantity of NFTs to purchase. + * @param recipient The recipient of the NFTs. + * @param amount The amount of tokens to purchase the NFTs with. + * @param signature The signature of the purchase request. + */ + function mintTo( + uint256 collectionId, + uint24 quantity, + address recipient, + uint256 amount, + bytes memory signature + ) + public + whenNotPaused + returns (uint256) + { + bytes32 hash = + keccak256(abi.encode(collectionId, quantity, recipient, amount, nonces[recipient])).toEthSignedMessageHash(); + if (!hasRole(SIGNER_ROLE, hash.recover(signature))) { + revert InvalidMint(MintErrorReason.INVALID_SIGNATURE); + } + + nonces[recipient]++; + return _mintTo(collectionId, quantity, amount, recipient); + } + + // ======================================================================== + // Batch transfer functions + // ======================================================================== + /** + * @notice Batch transfer NFTs from one address to another. + * @param from The address to transfer the NFTs from. + * @param to The address to transfer the NFTs to. + * @param tokenIds The IDs of the NFTs to transfer. + */ + function batchTransferFrom(address from, address to, uint256[] memory tokenIds) public whenNotPaused { + for (uint256 i = 0; i < tokenIds.length; i++) { + transferFrom(from, to, tokenIds[i]); + } + } + + /** + * @notice Batch safe transfer NFTs from one address to another. + * @param from The address to transfer the NFTs from. + * @param to The address to transfer the NFTs to. + * @param tokenIds The IDs of the NFTs to transfer. + */ + function batchSafeTransferFrom(address from, address to, uint256[] memory tokenIds) public whenNotPaused { + for (uint256 i = 0; i < tokenIds.length; i++) { + safeTransferFrom(from, to, tokenIds[i]); + } + } + + // ======================================================================== + // Claiming functions + // ======================================================================== + /** + * @notice upgrade token version to new version in case of claim event. + * @dev Restricted to TOKEN_UPGRADER_ROLE. + * @param tokenId The token ID to upgrade. + */ + function upgradeTokenVersion(uint256 tokenId) + external + onlyRole(TOKEN_UPGRADER_ROLE) + whenNotPaused + returns (uint256) + { + (uint256 collectionId, uint256 tokenVersion, uint256 number,) = TokenVersionUtil.getTokenInfo(tokenId); + tokenVersion++; + + if (!_collections[collectionId].exists) { + revert InvalidUpgrade(UpgradeErrorReason.INVALID_COLLECTION_ID); + } + + _requireMinted(tokenId); + + if (tokenVersion > TokenVersionUtil.MAX_VERSION) { + revert InvalidUpgrade(UpgradeErrorReason.VERSION_ID_TOO_HIGH); + } + + address owner = ownerOf(tokenId); + _burn(tokenId); // burn old token + + uint256 newTokenId = TokenVersionUtil.createTokenId(collectionId, tokenVersion, number); + _mint(owner, newTokenId); + return newTokenId; + } + + // ======================================================================== + // Rescue functions + // ======================================================================== + /** + * @notice Authorizes a rescue of a token by checking if the sender has the DEFAULT_ADMIN_ROLE + */ + function _authorizeRescue(uint256, address, string calldata) internal view override { + _checkRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /** + * @notice Rescues a single token by transferring it to a specified address + * @param tokenId The ID of the token to rescue + * @param recipient The address that received the rescued token + */ + function _rescue(uint256 tokenId, address recipient, string calldata) internal override { + _transfer(ownerOf(tokenId), recipient, tokenId); + } +} diff --git a/src/interfaces/IFANtiumAthletes.sol b/src/interfaces/IFANtiumAthletes.sol index 1b7949a..72875fa 100644 --- a/src/interfaces/IFANtiumAthletes.sol +++ b/src/interfaces/IFANtiumAthletes.sol @@ -109,7 +109,8 @@ enum MintErrorReason { COLLECTION_NOT_LAUNCHED, COLLECTION_PAUSED, ACCOUNT_NOT_KYCED, - INVALID_SIGNATURE + INVALID_SIGNATURE, + SIGNATURE_EXPIRED } enum UpgradeErrorReason { @@ -117,6 +118,20 @@ enum UpgradeErrorReason { VERSION_ID_TOO_HIGH } +struct VerificationStatus { + address account; + uint8 level; + uint256 expiresAt; +} + +struct MintRequest { + uint256 collectionId; + uint24 quantity; + address recipient; + uint256 amount; + VerificationStatus verificationStatus; +} + interface IFANtiumAthletes is IERC721Upgradeable { // ======================================================================== // Events @@ -163,17 +178,7 @@ interface IFANtiumAthletes is IERC721Upgradeable { // ======================================================================== // Minting // ======================================================================== - function mintTo(uint256 collectionId, uint24 quantity, address recipient) external returns (uint256); - - function mintTo( - uint256 collectionId, - uint24 quantity, - address recipient, - uint256 amount, - bytes memory signature - ) - external - returns (uint256); + function mintTo(MintRequest calldata mintRequest, bytes calldata signature) external returns (uint256); // ======================================================================== // Claiming diff --git a/test/FANtiumAthletesV10.fuzz.t.sol b/test/FANtiumAthletesV11.fuzz.t.sol similarity index 80% rename from test/FANtiumAthletesV10.fuzz.t.sol rename to test/FANtiumAthletesV11.fuzz.t.sol index 12598e2..e78cac6 100644 --- a/test/FANtiumAthletesV10.fuzz.t.sol +++ b/test/FANtiumAthletesV11.fuzz.t.sol @@ -2,11 +2,17 @@ pragma solidity 0.8.28; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import { Collection, CollectionData, IFANtiumAthletes } from "src/interfaces/IFANtiumAthletes.sol"; +import { + Collection, + CollectionData, + IFANtiumAthletes, + MintRequest, + VerificationStatus +} from "src/interfaces/IFANtiumAthletes.sol"; import { BaseTest } from "test/BaseTest.sol"; import { FANtiumAthletesFactory } from "test/setup/FANtiumAthletesFactory.sol"; -contract FANtiumAthletesV10FuzzTest is BaseTest, FANtiumAthletesFactory { +contract FANtiumAthletesV11FuzzTest is BaseTest, FANtiumAthletesFactory { function setUp() public override { FANtiumAthletesFactory.setUp(); } @@ -36,8 +42,27 @@ contract FANtiumAthletesV10FuzzTest is BaseTest, FANtiumAthletesFactory { vm.expectEmit(true, true, false, true, address(fantiumAthletes)); emit IFANtiumAthletes.Sale(collectionId, quantity, recipient, amountUSDC, 0); + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // create signature + bytes memory signature = typedSignPacked( + fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(mintRequest.verificationStatus) + ); + vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient); + uint256 lastTokenId = fantiumAthletes.mintTo(mintRequest, signature); assertEq(fantiumAthletes.ownerOf(lastTokenId), recipient); assertEq(usdc.balanceOf(recipient), recipientBalanceBefore - amountUSDC); @@ -81,8 +106,27 @@ contract FANtiumAthletesV10FuzzTest is BaseTest, FANtiumAthletesFactory { vm.expectEmit(true, true, false, true, address(fantiumAthletes)); emit IFANtiumAthletes.Sale(collectionId, quantity, recipient, amountUSDC, 0); + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // create signature + bytes memory signature = typedSignPacked( + fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(mintRequest.verificationStatus) + ); + vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient); + uint256 lastTokenId = fantiumAthletes.mintTo(mintRequest, signature); // Verify ownership for (uint256 i = 0; i < quantity; i++) { diff --git a/test/FANtiumAthletesV10.t.sol b/test/FANtiumAthletesV11.t.sol similarity index 90% rename from test/FANtiumAthletesV10.t.sol rename to test/FANtiumAthletesV11.t.sol index ed9e64f..37dfd6c 100644 --- a/test/FANtiumAthletesV10.t.sol +++ b/test/FANtiumAthletesV11.t.sol @@ -11,14 +11,17 @@ import { CollectionErrorReason, IFANtiumAthletes, MintErrorReason, - UpgradeErrorReason + MintRequest, + UpgradeErrorReason, + VerificationStatus } from "src/interfaces/IFANtiumAthletes.sol"; import { IRescue } from "src/interfaces/IRescue.sol"; import { TokenVersionUtil } from "src/utils/TokenVersionUtil.sol"; import { BaseTest } from "test/BaseTest.sol"; import { FANtiumAthletesFactory } from "test/setup/FANtiumAthletesFactory.sol"; +import { EIP712Signer } from "test/utils/EIP712Signer.sol"; -contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { +contract FANtiumAthletesV11Test is BaseTest, EIP712Signer, FANtiumAthletesFactory { using ECDSA for bytes32; using Strings for uint256; @@ -415,7 +418,8 @@ contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { function test_updateCollection_revert_decreasedMaxInvocations() public { uint256 collectionId = 1; - mintTo(collectionId, 10, recipient); // mint 10 tokens to increase invocations + uint24 quantity = 10; + mintTo(collectionId, quantity, recipient); // mint 10 tokens to increase invocations Collection memory currentCollection = fantiumAthletes.collections(collectionId); @@ -774,22 +778,45 @@ contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { assertEq(athleteAddress, collection.athleteAddress, "Incorrect athlete address"); } - // mintTo (standard price) - // ======================================================================== - function test_mintTo_standardPrice_ok_single() public { + // mintTo with KYC signature + // ======================================================================== + function test_mintTo_ok_single() public { uint256 collectionId = 1; // collection 1 is mintable uint24 quantity = 1; (uint256 amountUSDC,,,,) = prepareSale(collectionId, quantity, recipient); vm.expectEmit(true, true, false, true, address(fantiumAthletes)); emit IFANtiumAthletes.Sale(collectionId, quantity, recipient, amountUSDC, 0); - vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient); + vm.startPrank(recipient); + + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // create signature + bytes memory signature = + typedSignPacked(fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(status)); + + // mint + uint256 lastTokenId = fantiumAthletes.mintTo(mintRequest, signature); + vm.stopPrank(); + + // verify assertEq(fantiumAthletes.ownerOf(lastTokenId), recipient); } - function test_mintTo_standardPrice_ok_batch() public { + function test_mintTo_ok_batch() public { uint24 quantity = 10; uint256 collectionId = 1; // collection 1 is mintable @@ -810,12 +837,33 @@ contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { vm.expectEmit(true, true, false, true, address(fantiumAthletes)); emit IFANtiumAthletes.Sale(collectionId, quantity, recipient, amountUSDC, 0); + + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // create signature + bytes memory signature = + typedSignPacked(fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(status)); + + // mint vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient); + uint256 lastTokenId = fantiumAthletes.mintTo(mintRequest, signature); vm.stopPrank(); uint256 firstTokenId = lastTokenId - quantity + 1; + // verify for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; tokenId++) { assertEq(fantiumAthletes.ownerOf(tokenId), recipient); } @@ -823,37 +871,26 @@ contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { assertEq(usdc.balanceOf(recipient), recipientBalanceBefore - amountUSDC); } - // mintTo (custom price) - // ======================================================================== - function test_mintTo_customPrice_ok_single() public { + function test_mintTo_revert_invalidSignature() public { uint256 collectionId = 1; // collection 1 is mintable uint24 quantity = 1; - uint256 amountUSDC = 74 * 10 ** usdc.decimals(); // normal price is 99 USDC - (bytes memory signature,,,,,) = prepareSale(collectionId, quantity, recipient, amountUSDC); - - vm.expectEmit(true, true, false, true, address(fantiumAthletes)); - emit IFANtiumAthletes.Sale(collectionId, quantity, recipient, amountUSDC, 25 * 10 ** usdc.decimals()); - vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient, amountUSDC, signature); - - assertEq(fantiumAthletes.ownerOf(lastTokenId), recipient); - } + (uint256 amountUSDC,,,,) = prepareSale(collectionId, quantity, recipient); - function test_mintTo_customPrice_revert_malformedSignature() public { - uint256 collectionId = 1; // collection 1 is mintable - uint24 quantity = 1; - uint256 amountUSDC = 200; - bytes memory malformedSignature = abi.encodePacked("malformed signature"); + vm.startPrank(recipient); - vm.expectRevert("ECDSA: invalid signature length"); - vm.prank(recipient); - fantiumAthletes.mintTo(collectionId, quantity, recipient, amountUSDC, malformedSignature); - } + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); - function test_mintTo_customPrice_revert_invalidSigner() public { - uint256 collectionId = 1; // collection 1 is mintable - uint24 quantity = 1; - uint256 amountUSDC = 200; + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); bytes32 hash = keccak256(abi.encode(recipient, collectionId, quantity, amountUSDC, recipient)).toEthSignedMessageHash(); @@ -863,27 +900,82 @@ contract FANtiumAthletesV10Test is BaseTest, FANtiumAthletesFactory { vm.expectRevert( abi.encodeWithSelector(IFANtiumAthletes.InvalidMint.selector, MintErrorReason.INVALID_SIGNATURE) ); - vm.prank(recipient); - fantiumAthletes.mintTo(collectionId, quantity, recipient, amountUSDC, forgedSignature); + // mint + fantiumAthletes.mintTo(mintRequest, forgedSignature); + + vm.stopPrank(); } - function test_mintTo_revert_invalidNonce() public { + function test_mintTo_revert_accountNotKyced() public { uint256 collectionId = 1; // collection 1 is mintable uint24 quantity = 1; - uint256 amountUSDC = 200; - (bytes memory signature,,,,,) = prepareSale(collectionId, quantity, recipient, amountUSDC); + (uint256 amountUSDC,,,,) = prepareSale(collectionId, quantity, recipient); - // First mint pass, and nonce is incremented - vm.prank(recipient); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, recipient, amountUSDC, signature); - assertEq(fantiumAthletes.ownerOf(lastTokenId), recipient); + vm.startPrank(recipient); + + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 0, // no AML, no KYC + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // create signature + bytes memory signature = + typedSignPacked(fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(status)); - // Second mint fails, because nonce is incremented vm.expectRevert( - abi.encodeWithSelector(IFANtiumAthletes.InvalidMint.selector, MintErrorReason.INVALID_SIGNATURE) + abi.encodeWithSelector(IFANtiumAthletes.InvalidMint.selector, MintErrorReason.ACCOUNT_NOT_KYCED) ); - vm.prank(recipient); - fantiumAthletes.mintTo(collectionId, quantity, recipient, amountUSDC, signature); + // mint + fantiumAthletes.mintTo(mintRequest, signature); + + vm.stopPrank(); + } + + function test_mintTo_revert_signatureExpired() public { + uint256 collectionId = 1; // collection 1 is mintable + uint24 quantity = 1; + (uint256 amountUSDC,,,,) = prepareSale(collectionId, quantity, recipient); + + vm.startPrank(recipient); + + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // no AML, no KYC + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amountUSDC, + verificationStatus: status + }); + + // jump into the future + uint256 timeInFuture = status.expiresAt + 1 days; // by this time signature has expired + vm.warp(timeInFuture); + + // create signature + bytes memory signature = + typedSignPacked(fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(status)); + + vm.expectRevert( + abi.encodeWithSelector(IFANtiumAthletes.InvalidMint.selector, MintErrorReason.SIGNATURE_EXPIRED) + ); + // mint + fantiumAthletes.mintTo(mintRequest, signature); + + vm.stopPrank(); } // batchTransferFrom diff --git a/test/FANtiumMarketplaceV1.t.sol b/test/FANtiumMarketplaceV1.t.sol index 52a0ee7..31a628c 100644 --- a/test/FANtiumMarketplaceV1.t.sol +++ b/test/FANtiumMarketplaceV1.t.sol @@ -116,7 +116,7 @@ contract FANtiumMarketplaceV1Test is BaseTest, EIP712Signer, FANtiumMarketplaceF uint24 quantity = 1; prepareSale(collectionId, quantity, seller); vm.prank(seller); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, seller); // 1000000 + uint256 lastTokenId = mintTo(collectionId, quantity, seller); // 1000000 // seller should approve the token transfer vm.prank(seller); @@ -218,7 +218,7 @@ contract FANtiumMarketplaceV1Test is BaseTest, EIP712Signer, FANtiumMarketplaceF uint24 quantity = 1; prepareSale(collectionId, quantity, randomUser); vm.prank(randomUser); - uint256 lastTokenId = fantiumAthletes.mintTo(collectionId, quantity, randomUser); // 1000000 + uint256 lastTokenId = mintTo(collectionId, quantity, randomUser); // 1000000 // try to executeOffer using generated token id Offer memory offer = Offer({ diff --git a/test/setup/FANtiumAthletesFactory.sol b/test/setup/FANtiumAthletesFactory.sol index a52cb9c..5494b88 100644 --- a/test/setup/FANtiumAthletesFactory.sol +++ b/test/setup/FANtiumAthletesFactory.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; +import { EIP712Domain } from "../utils/EIP712Signer.sol"; import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { FANtiumAthletesV10 } from "src/FANtiumAthletesV10.sol"; -import { Collection, CollectionData } from "src/interfaces/IFANtiumAthletes.sol"; +import { FANtiumAthletesV11 } from "src/FANtiumAthletesV11.sol"; +import { Collection, CollectionData, MintRequest, VerificationStatus } from "src/interfaces/IFANtiumAthletes.sol"; import { UnsafeUpgrades } from "src/upgrades/UnsafeUpgrades.sol"; import { BaseTest } from "test/BaseTest.sol"; +import { EIP712Signer } from "test/utils/EIP712Signer.sol"; /** * @notice Collection data structure for deserialization. @@ -31,7 +33,7 @@ struct CollectionJson { uint256 tournamentEarningShare1e7; } -contract FANtiumAthletesFactory is BaseTest { +contract FANtiumAthletesFactory is BaseTest, EIP712Signer { using ECDSA for bytes32; address public fantiumAthletes_admin = makeAddr("admin"); @@ -46,17 +48,19 @@ contract FANtiumAthletesFactory is BaseTest { ERC20 public usdc; address public fantiumAthletes_implementation; address public fantiumAthletes_proxy; - FANtiumAthletesV10 public fantiumAthletes; + FANtiumAthletesV11 public fantiumAthletes; + + EIP712Domain athletesDomain; function setUp() public virtual { (fantiumAthletes_signer, fantiumAthletes_signerKey) = makeAddrAndKey("rewarder"); usdc = new ERC20("USD Coin", "USDC"); - fantiumAthletes_implementation = address(new FANtiumAthletesV10()); + fantiumAthletes_implementation = address(new FANtiumAthletesV11()); fantiumAthletes_proxy = UnsafeUpgrades.deployUUPSProxy( - fantiumAthletes_implementation, abi.encodeCall(FANtiumAthletesV10.initialize, (fantiumAthletes_admin)) + fantiumAthletes_implementation, abi.encodeCall(FANtiumAthletesV11.initialize, (fantiumAthletes_admin)) ); - fantiumAthletes = FANtiumAthletesV10(fantiumAthletes_proxy); + fantiumAthletes = FANtiumAthletesV11(fantiumAthletes_proxy); // Configure roles vm.startPrank(fantiumAthletes_admin); @@ -67,6 +71,10 @@ contract FANtiumAthletesFactory is BaseTest { fantiumAthletes.setBaseURI("https://app.fantium.com/api/metadata/"); fantiumAthletes.setTreasury(fantiumAthletes_treasuryPrimary); + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + fantiumAthletes.eip712Domain(); + athletesDomain = EIP712Domain(name, version, chainId, verifyingContract); + // Configure collections CollectionJson[] memory collections = abi.decode(loadFixture("collections.json"), (CollectionJson[])); for (uint256 i = 0; i < collections.length; i++) { @@ -160,10 +168,35 @@ contract FANtiumAthletesFactory is BaseTest { ); } + // can be used for testing when we don't test the mintTo implementation specifically, e.g. in Claiming contract function mintTo(uint256 collectionId, uint24 quantity, address recipient) public returns (uint256 lastTokenId) { - prepareSale(collectionId, quantity, recipient); - vm.prank(recipient); - return fantiumAthletes.mintTo(collectionId, quantity, recipient); + Collection memory collection = fantiumAthletes.collections(collectionId); + uint256 amount = collection.price * quantity * 10 ** usdc.decimals(); + + VerificationStatus memory status = VerificationStatus({ + account: recipient, + level: 1, // AML + expiresAt: 1_704_067_300 + }); + + MintRequest memory mintRequest = MintRequest({ + collectionId: collectionId, + quantity: quantity, + recipient: recipient, + amount: amount, + verificationStatus: status + }); + + // create signature + bytes memory signature = typedSignPacked( + fantiumAthletes_signerKey, athletesDomain, _hashVerificationStatus(mintRequest.verificationStatus) + ); + + // prepare sale and mint + prepareSale(mintRequest.collectionId, mintRequest.quantity, mintRequest.recipient); + vm.prank(mintRequest.recipient); + + return fantiumAthletes.mintTo(mintRequest, signature); } function signMint( @@ -181,4 +214,10 @@ contract FANtiumAthletesFactory is BaseTest { (uint8 v, bytes32 r, bytes32 s) = vm.sign(fantiumAthletes_signerKey, hash); return abi.encodePacked(r, s, v); } + + function _hashVerificationStatus(VerificationStatus memory status) internal view returns (bytes32) { + return keccak256( + abi.encode(fantiumAthletes.VERIFICATION_STATUS_TYPEHASH(), status.account, status.level, status.expiresAt) + ); + } }